summaryrefslogtreecommitdiff
path: root/script-beta/src
diff options
context:
space:
mode:
Diffstat (limited to 'script-beta/src')
-rw-r--r--script-beta/src/await.lua100
-rw-r--r--script-beta/src/brave/brave.lua70
-rw-r--r--script-beta/src/brave/init.lua4
-rw-r--r--script-beta/src/brave/log.lua52
-rw-r--r--script-beta/src/brave/work.lua55
-rw-r--r--script-beta/src/config.lua193
-rw-r--r--script-beta/src/core/definition.lua105
-rw-r--r--script-beta/src/core/diagnostics/ambiguity-1.lua69
-rw-r--r--script-beta/src/core/diagnostics/duplicate-index.lua62
-rw-r--r--script-beta/src/core/diagnostics/emmy-lua.lua3
-rw-r--r--script-beta/src/core/diagnostics/empty-block.lua49
-rw-r--r--script-beta/src/core/diagnostics/global-in-nil-env.lua66
-rw-r--r--script-beta/src/core/diagnostics/init.lua41
-rw-r--r--script-beta/src/core/diagnostics/lowercase-global.lua39
-rw-r--r--script-beta/src/core/diagnostics/newfield-call.lua37
-rw-r--r--script-beta/src/core/diagnostics/newline-call.lua38
-rw-r--r--script-beta/src/core/diagnostics/redefined-local.lua32
-rw-r--r--script-beta/src/core/diagnostics/redundant-parameter.lua102
-rw-r--r--script-beta/src/core/diagnostics/redundant-value.lua24
-rw-r--r--script-beta/src/core/diagnostics/trailing-space.lua55
-rw-r--r--script-beta/src/core/diagnostics/undefined-env-child.lua32
-rw-r--r--script-beta/src/core/diagnostics/undefined-global.lua63
-rw-r--r--script-beta/src/core/diagnostics/unused-function.lua45
-rw-r--r--script-beta/src/core/diagnostics/unused-label.lua22
-rw-r--r--script-beta/src/core/diagnostics/unused-local.lua46
-rw-r--r--script-beta/src/core/diagnostics/unused-vararg.lua31
-rw-r--r--script-beta/src/core/highlight.lua230
-rw-r--r--script-beta/src/core/hover/arg.lua20
-rw-r--r--script-beta/src/core/hover/init.lua56
-rw-r--r--script-beta/src/core/hover/label.lua103
-rw-r--r--script-beta/src/core/hover/name.lua64
-rw-r--r--script-beta/src/core/hover/return.lua34
-rw-r--r--script-beta/src/core/hover/table.lua35
-rw-r--r--script-beta/src/core/reference.lua84
-rw-r--r--script-beta/src/core/rename.lua374
-rw-r--r--script-beta/src/define/DiagnosticDefaultSeverity.lua21
-rw-r--r--script-beta/src/define/DiagnosticSeverity.lua6
-rw-r--r--script-beta/src/define/ErrorCodes.lua16
-rw-r--r--script-beta/src/doctor.lua380
-rw-r--r--script-beta/src/file-uri.lua108
-rw-r--r--script-beta/src/files.lua290
-rw-r--r--script-beta/src/fs-utility.lua314
-rw-r--r--script-beta/src/glob/gitignore.lua221
-rw-r--r--script-beta/src/glob/glob.lua122
-rw-r--r--script-beta/src/glob/init.lua4
-rw-r--r--script-beta/src/glob/matcher.lua151
-rw-r--r--script-beta/src/json/decode.lua153
-rw-r--r--script-beta/src/json/encode.lua135
-rw-r--r--script-beta/src/json/init.lua6
-rw-r--r--script-beta/src/jsonrpc.lua41
-rw-r--r--script-beta/src/language.lua137
-rw-r--r--script-beta/src/library.lua296
-rw-r--r--script-beta/src/log.lua140
-rw-r--r--script-beta/src/parser/ast.lua1738
-rw-r--r--script-beta/src/parser/calcline.lua93
-rw-r--r--script-beta/src/parser/compile.lua549
-rw-r--r--script-beta/src/parser/emmy.lua321
-rw-r--r--script-beta/src/parser/grammar.lua537
-rw-r--r--script-beta/src/parser/guide.lua621
-rw-r--r--script-beta/src/parser/init.lua11
-rw-r--r--script-beta/src/parser/lines.lua46
-rw-r--r--script-beta/src/parser/parse.lua45
-rw-r--r--script-beta/src/parser/relabel.lua361
-rw-r--r--script-beta/src/parser/split.lua9
-rw-r--r--script-beta/src/proto/define.lua140
-rw-r--r--script-beta/src/proto/init.lua3
-rw-r--r--script-beta/src/proto/proto.lua133
-rw-r--r--script-beta/src/provider/capability.lua42
-rw-r--r--script-beta/src/provider/completion.lua53
-rw-r--r--script-beta/src/provider/diagnostic.lua209
-rw-r--r--script-beta/src/provider/init.lua298
-rw-r--r--script-beta/src/provider/markdown.lua22
-rw-r--r--script-beta/src/pub/init.lua4
-rw-r--r--script-beta/src/pub/pub.lua236
-rw-r--r--script-beta/src/pub/report.lua21
-rw-r--r--script-beta/src/service/init.lua3
-rw-r--r--script-beta/src/service/service.lua137
-rw-r--r--script-beta/src/timer.lua218
-rw-r--r--script-beta/src/utility.lua452
-rw-r--r--script-beta/src/vm/dummySource.lua13
-rw-r--r--script-beta/src/vm/eachDef.lua65
-rw-r--r--script-beta/src/vm/eachField.lua169
-rw-r--r--script-beta/src/vm/eachRef.lua500
-rw-r--r--script-beta/src/vm/getGlobal.lua6
-rw-r--r--script-beta/src/vm/getGlobals.lua45
-rw-r--r--script-beta/src/vm/getLibrary.lua89
-rw-r--r--script-beta/src/vm/getLinks.lua48
-rw-r--r--script-beta/src/vm/getValue.lua895
-rw-r--r--script-beta/src/vm/init.lua11
-rw-r--r--script-beta/src/vm/special.lua0
-rw-r--r--script-beta/src/vm/vm.lua81
-rw-r--r--script-beta/src/workspace/init.lua3
-rw-r--r--script-beta/src/workspace/workspace.lua194
93 files changed, 13397 insertions, 0 deletions
diff --git a/script-beta/src/await.lua b/script-beta/src/await.lua
new file mode 100644
index 00000000..5a960e96
--- /dev/null
+++ b/script-beta/src/await.lua
@@ -0,0 +1,100 @@
+local timer = require 'timer'
+
+---@class await
+local m = {}
+m.type = 'await'
+
+m.coTracker = setmetatable({}, { __mode = 'k' })
+m.delayQueue = {}
+m.delayQueueIndex = 1
+
+--- 设置错误处理器
+---@param errHandle function {comment = '当有错误发生时,会以错误堆栈为参数调用该函数'}
+function m.setErrorHandle(errHandle)
+ m.errorHandle = errHandle
+end
+
+function m.checkResult(co, ...)
+ local suc, err = ...
+ if not suc and m.errorHandle then
+ m.errorHandle(debug.traceback(co, err))
+ end
+ return ...
+end
+
+--- 创建一个任务
+function m.create(callback, ...)
+ local co = coroutine.create(callback)
+ m.coTracker[co] = true
+ return m.checkResult(co, coroutine.resume(co, ...))
+end
+
+--- 休眠一段时间
+---@param time number
+function m.sleep(time, getVersion)
+ if not coroutine.isyieldable() then
+ if m.errorHandle then
+ m.errorHandle(debug.traceback('Cannot yield'))
+ end
+ return
+ end
+ local version = getVersion and getVersion()
+ local co = coroutine.running()
+ timer.wait(time, function ()
+ if version == (getVersion and getVersion()) then
+ return m.checkResult(co, coroutine.resume(co))
+ else
+ coroutine.close(co)
+ end
+ end)
+ return coroutine.yield(getVersion)
+end
+
+--- 等待直到唤醒
+---@param callback function
+function m.wait(callback, ...)
+ if not coroutine.isyieldable() then
+ return
+ end
+ local co = coroutine.running()
+ callback(function (...)
+ return m.checkResult(co, coroutine.resume(co, ...))
+ end)
+ return coroutine.yield(...)
+end
+
+--- 延迟
+function m.delay(getVersion)
+ if not coroutine.isyieldable() then
+ return
+ end
+ local co = coroutine.running()
+ local version = getVersion and getVersion()
+ m.delayQueue[#m.delayQueue+1] = function ()
+ if version == (getVersion and getVersion()) then
+ return m.checkResult(co, coroutine.resume(co))
+ else
+ coroutine.close(co)
+ end
+ end
+ return coroutine.yield()
+end
+
+--- 步进
+function m.step()
+ local waker = m.delayQueue[m.delayQueueIndex]
+ if waker then
+ m.delayQueue[m.delayQueueIndex] = false
+ m.delayQueueIndex = m.delayQueueIndex + 1
+ waker()
+ return true
+ else
+ for i = 1, #m.delayQueue do
+ m.delayQueue[i] = nil
+ end
+ m.delayQueueIndex = 1
+ return false
+ end
+end
+
+return m
diff --git a/script-beta/src/brave/brave.lua b/script-beta/src/brave/brave.lua
new file mode 100644
index 00000000..08909074
--- /dev/null
+++ b/script-beta/src/brave/brave.lua
@@ -0,0 +1,70 @@
+local thread = require 'bee.thread'
+
+---@class pub_brave
+local m = {}
+m.type = 'brave'
+m.ability = {}
+m.queue = {}
+
+--- 注册成为勇者
+function m.register(id)
+ m.taskpad = thread.channel('taskpad' .. id)
+ m.waiter = thread.channel('waiter' .. id)
+ m.id = id
+
+ if #m.queue > 0 then
+ for _, info in ipairs(m.queue) do
+ m.waiter:push(info.name, info.params)
+ end
+ end
+ m.queue = nil
+
+ m.start()
+end
+
+--- 注册能力
+function m.on(name, callback)
+ m.ability[name] = callback
+end
+
+--- 报告
+function m.push(name, params)
+ if m.waiter then
+ m.waiter:push(name, params)
+ else
+ m.queue[#m.queue+1] = {
+ name = name,
+ params = params,
+ }
+ end
+end
+
+--- 开始找工作
+function m.start()
+ m.push('mem', collectgarbage 'count')
+ while true do
+ local suc, name, id, params = m.taskpad:pop()
+ if not suc then
+ -- 找不到工作的勇者,只好睡觉
+ thread.sleep(0.001)
+ goto CONTINUE
+ end
+ local ability = m.ability[name]
+ -- TODO
+ if not ability then
+ m.waiter:push(id)
+ log.error('Brave can not handle this work: ' .. name)
+ goto CONTINUE
+ end
+ local ok, res = xpcall(ability, log.error, params)
+ if ok then
+ m.waiter:push(id, res)
+ else
+ m.waiter:push(id)
+ end
+ m.push('mem', collectgarbage 'count')
+ ::CONTINUE::
+ end
+end
+
+return m
diff --git a/script-beta/src/brave/init.lua b/script-beta/src/brave/init.lua
new file mode 100644
index 00000000..24c2e412
--- /dev/null
+++ b/script-beta/src/brave/init.lua
@@ -0,0 +1,4 @@
+local brave = require 'brave.brave'
+require 'brave.work'
+
+return brave
diff --git a/script-beta/src/brave/log.lua b/script-beta/src/brave/log.lua
new file mode 100644
index 00000000..cd27cd55
--- /dev/null
+++ b/script-beta/src/brave/log.lua
@@ -0,0 +1,52 @@
+local brave = require 'brave'
+
+local tablePack = table.pack
+local tostring = tostring
+local tableConcat = table.concat
+local debugTraceBack = debug.traceback
+local debugGetInfo = debug.getinfo
+
+_ENV = nil
+
+local function pushLog(level, ...)
+ local t = tablePack(...)
+ for i = 1, t.n do
+ t[i] = tostring(t[i])
+ end
+ local str = tableConcat(t, '\t', 1, t.n)
+ if level == 'error' then
+ str = str .. '\n' .. debugTraceBack(nil, 3)
+ end
+ local info = debugGetInfo(3, 'Sl')
+ brave.push('log', {
+ level = level,
+ msg = str,
+ src = info.source,
+ line = info.currentline,
+ })
+ return str
+end
+
+local m = {}
+
+function m.info(...)
+ pushLog('info', ...)
+end
+
+function m.debug(...)
+ pushLog('debug', ...)
+end
+
+function m.trace(...)
+ pushLog('trace', ...)
+end
+
+function m.warn(...)
+ pushLog('warn', ...)
+end
+
+function m.error(...)
+ pushLog('error', ...)
+end
+
+return m
diff --git a/script-beta/src/brave/work.lua b/script-beta/src/brave/work.lua
new file mode 100644
index 00000000..dba27808
--- /dev/null
+++ b/script-beta/src/brave/work.lua
@@ -0,0 +1,55 @@
+local brave = require 'brave.brave'
+local jsonrpc = require 'jsonrpc'
+local parser = require 'parser'
+local fs = require 'bee.filesystem'
+local furi = require 'file-uri'
+local util = require 'utility'
+
+brave.on('loadProto', function ()
+ while true do
+ local proto = jsonrpc.decode(io.read, log.error)
+ if proto then
+ brave.push('proto', proto)
+ end
+ end
+end)
+
+brave.on('compile', function (text)
+ local state, err = parser:compile(text, 'lua', 'Lua 5.4')
+ if not state then
+ log.error(err)
+ return
+ end
+ local lines = parser:lines(text)
+ return {
+ root = state.root,
+ value = state.value,
+ errs = state.errs,
+ lines = lines,
+ }
+end)
+
+brave.on('listDirectory', function (uri)
+ local path = fs.path(furi.decode(uri))
+ local uris = {}
+ for child in path:list_directory() do
+ local childUri = furi.encode(child:string())
+ uris[#uris+1] = childUri
+ end
+ return uris
+end)
+
+brave.on('isDirectory', function (uri)
+ local path = fs.path(furi.decode(uri))
+ return fs.is_directory(path)
+end)
+
+brave.on('loadFile', function (uri)
+ local filename = furi.decode(uri)
+ return util.loadFile(filename)
+end)
+
+brave.on('saveFile', function (params)
+ local filename = furi.decode(params.uri)
+ return util.saveFile(filename, params.text)
+end)
diff --git a/script-beta/src/config.lua b/script-beta/src/config.lua
new file mode 100644
index 00000000..758402b0
--- /dev/null
+++ b/script-beta/src/config.lua
@@ -0,0 +1,193 @@
+local util = require 'utility'
+local DiagnosticDefaultSeverity = require 'define.DiagnosticDefaultSeverity'
+
+local m = {}
+m.version = 0
+
+local function Boolean(v)
+ if type(v) == 'boolean' then
+ return true, v
+ end
+ return false
+end
+
+local function Integer(v)
+ if type(v) == 'number' then
+ return true, math.floor(v)
+ end
+ return false
+end
+
+local function String(v)
+ return true, tostring(v)
+end
+
+local function Str2Hash(sep)
+ return function (v)
+ if type(v) == 'string' then
+ local t = {}
+ for s in v:gmatch('[^'..sep..']+') do
+ t[s] = true
+ end
+ return true, t
+ end
+ if type(v) == 'table' then
+ local t = {}
+ for _, s in ipairs(v) do
+ if type(s) == 'string' then
+ t[s] = true
+ end
+ end
+ return true, t
+ end
+ return false
+ end
+end
+
+local function Array(checker)
+ return function (tbl)
+ if type(tbl) ~= 'table' then
+ return false
+ end
+ local t = {}
+ for _, v in ipairs(tbl) do
+ local ok, result = checker(v)
+ if ok then
+ t[#t+1] = result
+ end
+ end
+ return true, t
+ end
+end
+
+local function Hash(keyChecker, valueChecker)
+ return function (tbl)
+ if type(tbl) ~= 'table' then
+ return false
+ end
+ local t = {}
+ for k, v in pairs(tbl) do
+ local ok1, key = keyChecker(k)
+ local ok2, value = valueChecker(v)
+ if ok1 and ok2 then
+ t[key] = value
+ end
+ end
+ if not next(t) then
+ return false
+ end
+ return true, t
+ end
+end
+
+local function Or(...)
+ local checkers = {...}
+ return function (obj)
+ for _, checker in ipairs(checkers) do
+ local suc, res = checker(obj)
+ if suc then
+ return true, res
+ end
+ end
+ return false
+ end
+end
+
+local ConfigTemplate = {
+ runtime = {
+ version = {'Lua 5.3', String},
+ library = {{}, Str2Hash ';'},
+ path = {{
+ "?.lua",
+ "?/init.lua",
+ "?/?.lua"
+ }, Array(String)},
+ },
+ diagnostics = {
+ enable = {true, Boolean},
+ globals = {{}, Str2Hash ';'},
+ disable = {{}, Str2Hash ';'},
+ severity = {
+ util.deepCopy(DiagnosticDefaultSeverity),
+ Hash(String, String),
+ },
+ },
+ workspace = {
+ ignoreDir = {{}, Str2Hash ';'},
+ ignoreSubmodules= {true, Boolean},
+ useGitIgnore = {true, Boolean},
+ maxPreload = {300, Integer},
+ preloadFileSize = {100, Integer},
+ library = {{}, Hash(
+ String,
+ Or(Boolean, Array(String))
+ )}
+ },
+ completion = {
+ enable = {true, Boolean},
+ callSnippet = {'Both', String},
+ keywordSnippet = {'Both', String},
+ },
+ plugin = {
+ enable = {false, Boolean},
+ path = {'.vscode/lua-plugin/*.lua', String},
+ },
+}
+
+local OtherTemplate = {
+ associations = {{}, Hash(String, String)},
+ exclude = {{}, Hash(String, Boolean)},
+}
+
+local function init()
+ if m.config then
+ return
+ end
+
+ m.config = {}
+ for c, t in pairs(ConfigTemplate) do
+ m.config[c] = {}
+ for k, info in pairs(t) do
+ m.config[c][k] = info[1]
+ end
+ end
+
+ m.other = {}
+ for k, info in pairs(OtherTemplate) do
+ m.other[k] = info[1]
+ end
+end
+
+function m.setConfig(config, other)
+ m.version = m.version + 1
+ xpcall(function ()
+ for c, t in pairs(config) do
+ for k, v in pairs(t) do
+ local region = ConfigTemplate[c]
+ if region then
+ local info = region[k]
+ local suc, v = info[2](v)
+ if suc then
+ m.config[c][k] = v
+ else
+ m.config[c][k] = info[1]
+ end
+ end
+ end
+ end
+ for k, v in pairs(other) do
+ local info = OtherTemplate[k]
+ local suc, v = info[2](v)
+ if suc then
+ m.other[k] = v
+ else
+ m.other[k] = info[1]
+ end
+ end
+ log.debug('Config update: ', util.dump(m.config), util.dump(m.other))
+ end, log.error)
+end
+
+init()
+
+return m
diff --git a/script-beta/src/core/definition.lua b/script-beta/src/core/definition.lua
new file mode 100644
index 00000000..865fc7cb
--- /dev/null
+++ b/script-beta/src/core/definition.lua
@@ -0,0 +1,105 @@
+local guide = require 'parser.guide'
+local workspace = require 'workspace'
+local files = require 'files'
+local vm = require 'vm'
+
+local function findDef(source, callback)
+ if source.type ~= 'local'
+ and source.type ~= 'getlocal'
+ and source.type ~= 'setlocal'
+ and source.type ~= 'setglobal'
+ and source.type ~= 'getglobal'
+ and source.type ~= 'field'
+ and source.type ~= 'method'
+ and source.type ~= 'string'
+ and source.type ~= 'number'
+ and source.type ~= 'boolean'
+ and source.type ~= 'goto' then
+ return
+ end
+ vm.eachDef(source, function (info)
+ if info.mode == 'declare'
+ or info.mode == 'set'
+ or info.mode == 'return' then
+ local src = info.source
+ local root = guide.getRoot(src)
+ local uri = root.uri
+ if src.type == 'setfield'
+ or src.type == 'getfield'
+ or src.type == 'tablefield' then
+ callback(src.field, uri)
+ elseif src.type == 'setindex'
+ or src.type == 'getindex'
+ or src.type == 'tableindex' then
+ callback(src.index, uri)
+ elseif src.type == 'getmethod'
+ or src.type == 'setmethod' then
+ callback(src.method, uri)
+ else
+ callback(src, uri)
+ end
+ end
+ end)
+end
+
+local function checkRequire(source, offset, callback)
+ if source.type ~= 'call' then
+ return
+ end
+ local func = source.node
+ local pathSource = source.args and source.args[1]
+ if not pathSource then
+ return
+ end
+ if not guide.isContain(pathSource, offset) then
+ return
+ end
+ local literal = guide.getLiteral(pathSource)
+ if type(literal) ~= 'string' then
+ return
+ end
+ local name = func.special
+ if name == 'require' then
+ local result = workspace.findUrisByRequirePath(literal, true)
+ for _, uri in ipairs(result) do
+ callback(uri)
+ end
+ elseif name == 'dofile'
+ or name == 'loadfile' then
+ local result = workspace.findUrisByFilePath(literal, true)
+ for _, uri in ipairs(result) do
+ callback(uri)
+ end
+ end
+end
+
+return function (uri, offset)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local results = {}
+ guide.eachSourceContain(ast.ast, offset, function (source)
+ checkRequire(source, offset, function (uri)
+ results[#results+1] = {
+ uri = files.getOriginUri(uri),
+ source = source,
+ target = {
+ start = 0,
+ finish = 0,
+ }
+ }
+ end)
+ findDef(source, function (target, uri)
+ results[#results+1] = {
+ target = target,
+ uri = files.getOriginUri(uri),
+ source = source,
+ }
+ end)
+ end)
+ if #results == 0 then
+ return nil
+ end
+ return results
+end
diff --git a/script-beta/src/core/diagnostics/ambiguity-1.lua b/script-beta/src/core/diagnostics/ambiguity-1.lua
new file mode 100644
index 00000000..37815fb5
--- /dev/null
+++ b/script-beta/src/core/diagnostics/ambiguity-1.lua
@@ -0,0 +1,69 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+
+local opMap = {
+ ['+'] = true,
+ ['-'] = true,
+ ['*'] = true,
+ ['/'] = true,
+ ['//'] = true,
+ ['^'] = true,
+ ['<<'] = true,
+ ['>>'] = true,
+ ['&'] = true,
+ ['|'] = true,
+ ['~'] = true,
+ ['..'] = true,
+}
+
+local literalMap = {
+ ['number'] = true,
+ ['boolean'] = true,
+ ['string'] = true,
+ ['table'] = true,
+}
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ local text = files.getText(uri)
+ guide.eachSourceType(ast.ast, 'binary', function (source)
+ if source.op.type ~= 'or' then
+ return
+ end
+ local first = source[1]
+ local second = source[2]
+ -- a + (b or 0) --> (a + b) or 0
+ do
+ if opMap[first.op and first.op.type]
+ and first.type ~= 'unary'
+ and not second.op
+ and literalMap[second.type]
+ and not literalMap[first[2].type]
+ then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ message = lang.script('DIAG_AMBIGUITY_1', text:sub(first.start, first.finish))
+ }
+ end
+ end
+ -- (a or 0) + c --> a or (0 + c)
+ do
+ if opMap[second.op and second.op.type]
+ and second.type ~= 'unary'
+ and not first.op
+ and literalMap[second[1].type]
+ then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ message = lang.script('DIAG_AMBIGUITY_1', text:sub(second.start, second.finish))
+ }
+ end
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/duplicate-index.lua b/script-beta/src/core/diagnostics/duplicate-index.lua
new file mode 100644
index 00000000..76b1c958
--- /dev/null
+++ b/script-beta/src/core/diagnostics/duplicate-index.lua
@@ -0,0 +1,62 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+local define = require 'proto.define'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ guide.eachSourceType(ast.ast, 'table', function (source)
+ local mark = {}
+ for _, obj in ipairs(source) do
+ if obj.type == 'tablefield'
+ or obj.type == 'tableindex' then
+ local name = guide.getKeyName(obj)
+ if name then
+ if not mark[name] then
+ mark[name] = {}
+ end
+ mark[name][#mark[name]+1] = obj.field or obj.index
+ end
+ end
+ end
+
+ for name, defs in pairs(mark) do
+ local sname = name:match '^.|(.+)$'
+ if #defs > 1 and sname then
+ local related = {}
+ for i = 1, #defs do
+ local def = defs[i]
+ related[i] = {
+ start = def.start,
+ finish = def.finish,
+ uri = uri,
+ }
+ end
+ for i = 1, #defs - 1 do
+ local def = defs[i]
+ callback {
+ start = def.start,
+ finish = def.finish,
+ related = related,
+ message = lang.script('DIAG_DUPLICATE_INDEX', sname),
+ level = define.DiagnosticSeverity.Hint,
+ tags = { define.DiagnosticTag.Unnecessary },
+ }
+ end
+ for i = #defs, #defs do
+ local def = defs[i]
+ callback {
+ start = def.start,
+ finish = def.finish,
+ related = related,
+ message = lang.script('DIAG_DUPLICATE_INDEX', sname),
+ }
+ end
+ end
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/emmy-lua.lua b/script-beta/src/core/diagnostics/emmy-lua.lua
new file mode 100644
index 00000000..b3d19c21
--- /dev/null
+++ b/script-beta/src/core/diagnostics/emmy-lua.lua
@@ -0,0 +1,3 @@
+return function ()
+
+end
diff --git a/script-beta/src/core/diagnostics/empty-block.lua b/script-beta/src/core/diagnostics/empty-block.lua
new file mode 100644
index 00000000..2024f4e3
--- /dev/null
+++ b/script-beta/src/core/diagnostics/empty-block.lua
@@ -0,0 +1,49 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+local define = require 'proto.define'
+
+-- 检查空代码块
+-- 但是排除忙等待(repeat/while)
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ guide.eachSourceType(ast.ast, 'if', function (source)
+ for _, block in ipairs(source) do
+ if #block > 0 then
+ return
+ end
+ end
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script.DIAG_EMPTY_BLOCK,
+ }
+ end)
+ guide.eachSourceType(ast.ast, 'loop', function (source)
+ if #source > 0 then
+ return
+ end
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script.DIAG_EMPTY_BLOCK,
+ }
+ end)
+ guide.eachSourceType(ast.ast, 'in', function (source)
+ if #source > 0 then
+ return
+ end
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script.DIAG_EMPTY_BLOCK,
+ }
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/global-in-nil-env.lua b/script-beta/src/core/diagnostics/global-in-nil-env.lua
new file mode 100644
index 00000000..9a0d4f35
--- /dev/null
+++ b/script-beta/src/core/diagnostics/global-in-nil-env.lua
@@ -0,0 +1,66 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+
+-- TODO: 检查路径是否可达
+local function mayRun(path)
+ return true
+end
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ local root = guide.getRoot(ast.ast)
+ local env = guide.getENV(root)
+
+ local nilDefs = {}
+ if not env.ref then
+ return
+ end
+ for _, ref in ipairs(env.ref) do
+ if ref.type == 'setlocal' then
+ if ref.value and ref.value.type == 'nil' then
+ nilDefs[#nilDefs+1] = ref
+ end
+ end
+ end
+
+ if #nilDefs == 0 then
+ return
+ end
+
+ local function check(source)
+ local node = source.node
+ if node.tag == '_ENV' then
+ local ok
+ for _, nilDef in ipairs(nilDefs) do
+ local mode, pathA = guide.getPath(nilDef, source)
+ if mode == 'before'
+ and mayRun(pathA) then
+ ok = nilDef
+ break
+ end
+ end
+ if ok then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ uri = uri,
+ message = lang.script.DIAG_GLOBAL_IN_NIL_ENV,
+ related = {
+ {
+ start = ok.start,
+ finish = ok.finish,
+ uri = uri,
+ }
+ }
+ }
+ end
+ end
+ end
+
+ guide.eachSourceType(ast.ast, 'getglobal', check)
+ guide.eachSourceType(ast.ast, 'setglobal', check)
+end
diff --git a/script-beta/src/core/diagnostics/init.lua b/script-beta/src/core/diagnostics/init.lua
new file mode 100644
index 00000000..0d523f26
--- /dev/null
+++ b/script-beta/src/core/diagnostics/init.lua
@@ -0,0 +1,41 @@
+local files = require 'files'
+local define = require 'proto.define'
+local config = require 'config'
+local await = require 'await'
+
+local function check(uri, name, level, results)
+ if config.config.diagnostics.disable[name] then
+ return
+ end
+ level = config.config.diagnostics.severity[name] or level
+ local severity = define.DiagnosticSeverity[level]
+ local clock = os.clock()
+ require('core.diagnostics.' .. name)(uri, function (result)
+ result.level = severity or result.level
+ result.code = name
+ results[#results+1] = result
+ end, name)
+ local passed = os.clock() - clock
+ if passed >= 0.5 then
+ log.warn(('Diagnostics [%s] @ [%s] takes [%.3f] sec!'):format(name, uri, passed))
+ await.delay()
+ end
+end
+
+return function (uri)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local results = {}
+
+ for name, level in pairs(define.DiagnosticDefaultSeverity) do
+ check(uri, name, level, results)
+ end
+
+ if #results == 0 then
+ return nil
+ end
+
+ return results
+end
diff --git a/script-beta/src/core/diagnostics/lowercase-global.lua b/script-beta/src/core/diagnostics/lowercase-global.lua
new file mode 100644
index 00000000..bc48e1e6
--- /dev/null
+++ b/script-beta/src/core/diagnostics/lowercase-global.lua
@@ -0,0 +1,39 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+local config = require 'config'
+local library = require 'library'
+
+-- 不允许定义首字母小写的全局变量(很可能是拼错或者漏删)
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ local definedGlobal = {}
+ for name in pairs(config.config.diagnostics.globals) do
+ definedGlobal[name] = true
+ end
+ for name in pairs(library.global) do
+ definedGlobal[name] = true
+ end
+
+ guide.eachSourceType(ast.ast, 'setglobal', function (source)
+ local name = guide.getName(source)
+ if definedGlobal[name] then
+ return
+ end
+ local first = name:match '%w'
+ if not first then
+ return
+ end
+ if first:match '%l' then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ message = lang.script.DIAG_LOWERCASE_GLOBAL,
+ }
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/newfield-call.lua b/script-beta/src/core/diagnostics/newfield-call.lua
new file mode 100644
index 00000000..75681cbc
--- /dev/null
+++ b/script-beta/src/core/diagnostics/newfield-call.lua
@@ -0,0 +1,37 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+
+ guide.eachSourceType(ast.ast, 'table', function (source)
+ for i = 1, #source do
+ local field = source[i]
+ if field.type == 'call' then
+ local func = field.node
+ local args = field.args
+ if args then
+ local funcLine = guide.positionOf(lines, func.finish)
+ local argsLine = guide.positionOf(lines, args.start)
+ if argsLine > funcLine then
+ callback {
+ start = field.start,
+ finish = field.finish,
+ message = lang.script('DIAG_PREFIELD_CALL'
+ , text:sub(func.start, func.finish)
+ , text:sub(args.start, args.finish)
+ )
+ }
+ end
+ end
+ end
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/newline-call.lua b/script-beta/src/core/diagnostics/newline-call.lua
new file mode 100644
index 00000000..cb318380
--- /dev/null
+++ b/script-beta/src/core/diagnostics/newline-call.lua
@@ -0,0 +1,38 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ local lines = files.getLines(uri)
+
+ guide.eachSourceType(ast.ast, 'call', function (source)
+ local node = source.node
+ local args = source.args
+ if not args then
+ return
+ end
+
+ -- 必须有其他人在继续使用当前对象
+ if not source.next then
+ return
+ end
+
+ local nodeRow = guide.positionOf(lines, node.finish)
+ local argRow = guide.positionOf(lines, args.start)
+ if nodeRow == argRow then
+ return
+ end
+
+ if #args == 1 then
+ callback {
+ start = args.start,
+ finish = args.finish,
+ message = lang.script.DIAG_PREVIOUS_CALL,
+ }
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/redefined-local.lua b/script-beta/src/core/diagnostics/redefined-local.lua
new file mode 100644
index 00000000..f6176794
--- /dev/null
+++ b/script-beta/src/core/diagnostics/redefined-local.lua
@@ -0,0 +1,32 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ guide.eachSourceType(ast.ast, 'local', function (source)
+ local name = source[1]
+ if name == '_'
+ or name == '_ENV' then
+ return
+ end
+ local exist = guide.getLocal(source, name, source.start-1)
+ if exist then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ message = lang.script('DIAG_REDEFINED_LOCAL', name),
+ related = {
+ {
+ start = exist.start,
+ finish = exist.finish,
+ uri = uri,
+ }
+ },
+ }
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/redundant-parameter.lua b/script-beta/src/core/diagnostics/redundant-parameter.lua
new file mode 100644
index 00000000..ec14188e
--- /dev/null
+++ b/script-beta/src/core/diagnostics/redundant-parameter.lua
@@ -0,0 +1,102 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local vm = require 'vm'
+local lang = require 'language'
+local define = require 'proto.define'
+local await = require 'await'
+
+local function countLibraryArgs(source)
+ local func = vm.getLibrary(source)
+ if not func then
+ return nil
+ end
+ local result = 0
+ if not func.args then
+ return result
+ end
+ if func.args[#func.args].type == '...' then
+ return math.maxinteger
+ end
+ result = result + #func.args
+ return result
+end
+
+local function countCallArgs(source)
+ local result = 0
+ if not source.args then
+ return 0
+ end
+ if source.node and source.node.type == 'getmethod' then
+ result = result + 1
+ end
+ result = result + #source.args
+ return result
+end
+
+local function countFuncArgs(source)
+ local result = 0
+ if not source.args then
+ return result
+ end
+ if source.args[#source.args].type == '...' then
+ return math.maxinteger
+ end
+ if source.parent and source.parent.type == 'setmethod' then
+ result = result + 1
+ end
+ result = result + #source.args
+ return result
+end
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ guide.eachSourceType(ast.ast, 'call', function (source)
+ local callArgs = countCallArgs(source)
+ if callArgs == 0 then
+ return
+ end
+
+ await.delay(function ()
+ return files.globalVersion
+ end)
+
+ local func = source.node
+ local funcArgs
+ vm.eachDef(func, function (info)
+ if info.mode == 'value' then
+ local src = info.source
+ if src.type == 'function' then
+ local args = countFuncArgs(src)
+ if not funcArgs or args > funcArgs then
+ funcArgs = args
+ end
+ end
+ end
+ end)
+
+ funcArgs = funcArgs or countLibraryArgs(func)
+ if not funcArgs then
+ return
+ end
+
+ local delta = callArgs - funcArgs
+ if delta <= 0 then
+ return
+ end
+ for i = #source.args - delta + 1, #source.args do
+ local arg = source.args[i]
+ if arg then
+ callback {
+ start = arg.start,
+ finish = arg.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script('DIAG_OVER_MAX_ARGS', funcArgs, callArgs)
+ }
+ end
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/redundant-value.lua b/script-beta/src/core/diagnostics/redundant-value.lua
new file mode 100644
index 00000000..be483448
--- /dev/null
+++ b/script-beta/src/core/diagnostics/redundant-value.lua
@@ -0,0 +1,24 @@
+local files = require 'files'
+local define = require 'proto.define'
+local lang = require 'language'
+
+return function (uri, callback, code)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ local diags = ast.diags[code]
+ if not diags then
+ return
+ end
+
+ for _, info in ipairs(diags) do
+ callback {
+ start = info.start,
+ finish = info.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script('DIAG_OVER_MAX_VALUES', info.max, info.passed)
+ }
+ end
+end
diff --git a/script-beta/src/core/diagnostics/trailing-space.lua b/script-beta/src/core/diagnostics/trailing-space.lua
new file mode 100644
index 00000000..e54a6e60
--- /dev/null
+++ b/script-beta/src/core/diagnostics/trailing-space.lua
@@ -0,0 +1,55 @@
+local files = require 'files'
+local lang = require 'language'
+local guide = require 'parser.guide'
+
+local function isInString(ast, offset)
+ local result = false
+ guide.eachSourceType(ast, 'string', function (source)
+ if offset >= source.start and offset <= source.finish then
+ result = true
+ end
+ end)
+ return result
+end
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ local text = files.getText(uri)
+ local lines = files.getLines(uri)
+ for i = 1, #lines do
+ local start = lines[i].start
+ local range = lines[i].range
+ local lastChar = text:sub(range, range)
+ if lastChar ~= ' ' and lastChar ~= '\t' then
+ goto NEXT_LINE
+ end
+ if isInString(ast.ast, range) then
+ goto NEXT_LINE
+ end
+ local first = start
+ for n = range - 1, start, -1 do
+ local char = text:sub(n, n)
+ if char ~= ' ' and char ~= '\t' then
+ first = n + 1
+ break
+ end
+ end
+ if first == start then
+ callback {
+ start = first,
+ finish = range,
+ message = lang.script.DIAG_LINE_ONLY_SPACE,
+ }
+ else
+ callback {
+ start = first,
+ finish = range,
+ message = lang.script.DIAG_LINE_POST_SPACE,
+ }
+ end
+ ::NEXT_LINE::
+ end
+end
diff --git a/script-beta/src/core/diagnostics/undefined-env-child.lua b/script-beta/src/core/diagnostics/undefined-env-child.lua
new file mode 100644
index 00000000..df096cb8
--- /dev/null
+++ b/script-beta/src/core/diagnostics/undefined-env-child.lua
@@ -0,0 +1,32 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local vm = require 'vm'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ -- 再遍历一次 getglobal ,找出 _ENV 被重载的情况
+ guide.eachSourceType(ast.ast, 'getglobal', function (source)
+ -- 单独验证自己是否在重载过的 _ENV 中有定义
+ if source.node.tag == '_ENV' then
+ return
+ end
+ local setInENV = vm.eachRef(source, function (info)
+ if info.mode == 'set' then
+ return true
+ end
+ end)
+ if setInENV then
+ return
+ end
+ local key = source[1]
+ callback {
+ start = source.start,
+ finish = source.finish,
+ message = lang.script('DIAG_UNDEF_ENV_CHILD', key),
+ }
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/undefined-global.lua b/script-beta/src/core/diagnostics/undefined-global.lua
new file mode 100644
index 00000000..ed81ced3
--- /dev/null
+++ b/script-beta/src/core/diagnostics/undefined-global.lua
@@ -0,0 +1,63 @@
+local files = require 'files'
+local vm = require 'vm'
+local lang = require 'language'
+local library = require 'library'
+local config = require 'config'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ local globalCache = {}
+
+ -- 遍历全局变量,检查所有没有 mode['set'] 的全局变量
+ local globals = vm.getGlobals(ast.ast)
+ for key, infos in pairs(globals) do
+ if infos.mode['set'] == true then
+ goto CONTINUE
+ end
+ if globalCache[key] then
+ goto CONTINUE
+ end
+ local skey = key and key:match '^s|(.+)$'
+ if not skey then
+ goto CONTINUE
+ end
+ if library.global[skey] then
+ goto CONTINUE
+ end
+ if config.config.diagnostics.globals[skey] then
+ goto CONTINUE
+ end
+ if globalCache[key] == nil then
+ local uris = files.findGlobals(key)
+ for i = 1, #uris do
+ local destAst = files.getAst(uris[i])
+ local destGlobals = vm.getGlobals(destAst.ast)
+ if destGlobals[key] and destGlobals[key].mode['set'] then
+ globalCache[key] = true
+ goto CONTINUE
+ end
+ end
+ end
+ globalCache[key] = false
+ local message = lang.script('DIAG_UNDEF_GLOBAL', skey)
+ local otherVersion = library.other[skey]
+ local customVersion = library.custom[skey]
+ if otherVersion then
+ message = ('%s(%s)'):format(message, lang.script('DIAG_DEFINED_VERSION', table.concat(otherVersion, '/'), config.config.runtime.version))
+ elseif customVersion then
+ message = ('%s(%s)'):format(message, lang.script('DIAG_DEFINED_CUSTOM', table.concat(customVersion, '/')))
+ end
+ for _, info in ipairs(infos) do
+ callback {
+ start = info.source.start,
+ finish = info.source.finish,
+ message = message,
+ }
+ end
+ ::CONTINUE::
+ end
+end
diff --git a/script-beta/src/core/diagnostics/unused-function.lua b/script-beta/src/core/diagnostics/unused-function.lua
new file mode 100644
index 00000000..6c53cdf7
--- /dev/null
+++ b/script-beta/src/core/diagnostics/unused-function.lua
@@ -0,0 +1,45 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local vm = require 'vm'
+local define = require 'proto.define'
+local lang = require 'language'
+local await = require 'await'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ -- 只检查局部函数与全局函数
+ guide.eachSourceType(ast.ast, 'function', function (source)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ if parent.type ~= 'local'
+ and parent.type ~= 'setlocal'
+ and parent.type ~= 'setglobal' then
+ return
+ end
+ local hasSet
+ local hasGet = vm.eachRef(source, function (info)
+ if info.mode == 'get' then
+ return true
+ elseif info.mode == 'set'
+ or info.mode == 'declare' then
+ hasSet = true
+ end
+ end)
+ if not hasGet and hasSet then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script.DIAG_UNUSED_FUNCTION,
+ }
+ end
+ await.delay(function ()
+ return files.globalVersion
+ end)
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/unused-label.lua b/script-beta/src/core/diagnostics/unused-label.lua
new file mode 100644
index 00000000..e6d998ba
--- /dev/null
+++ b/script-beta/src/core/diagnostics/unused-label.lua
@@ -0,0 +1,22 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local define = require 'proto.define'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ guide.eachSourceType(ast.ast, 'label', function (source)
+ if not source.ref then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script('DIAG_UNUSED_LABEL', source[1]),
+ }
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/unused-local.lua b/script-beta/src/core/diagnostics/unused-local.lua
new file mode 100644
index 00000000..22b2e16b
--- /dev/null
+++ b/script-beta/src/core/diagnostics/unused-local.lua
@@ -0,0 +1,46 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local define = require 'proto.define'
+local lang = require 'language'
+
+local function hasGet(loc)
+ if not loc.ref then
+ return false
+ end
+ for _, ref in ipairs(loc.ref) do
+ if ref.type == 'getlocal' then
+ if not ref.next then
+ return true
+ end
+ local nextType = ref.next.type
+ if nextType ~= 'setmethod'
+ and nextType ~= 'setfield'
+ and nextType ~= 'setindex' then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+ guide.eachSourceType(ast.ast, 'local', function (source)
+ local name = source[1]
+ if name == '_'
+ or name == '_ENV' then
+ return
+ end
+ if not hasGet(source) then
+ callback {
+ start = source.start,
+ finish = source.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script('DIAG_UNUSED_LOCAL', name),
+ }
+ end
+ end)
+end
diff --git a/script-beta/src/core/diagnostics/unused-vararg.lua b/script-beta/src/core/diagnostics/unused-vararg.lua
new file mode 100644
index 00000000..74cc08e7
--- /dev/null
+++ b/script-beta/src/core/diagnostics/unused-vararg.lua
@@ -0,0 +1,31 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local define = require 'proto.define'
+local lang = require 'language'
+
+return function (uri, callback)
+ local ast = files.getAst(uri)
+ if not ast then
+ return
+ end
+
+ guide.eachSourceType(ast.ast, 'function', function (source)
+ local args = source.args
+ if not args then
+ return
+ end
+
+ for _, arg in ipairs(args) do
+ if arg.type == '...' then
+ if not arg.ref then
+ callback {
+ start = arg.start,
+ finish = arg.finish,
+ tags = { define.DiagnosticTag.Unnecessary },
+ message = lang.script.DIAG_UNUSED_VARARG,
+ }
+ end
+ end
+ end
+ end)
+end
diff --git a/script-beta/src/core/highlight.lua b/script-beta/src/core/highlight.lua
new file mode 100644
index 00000000..61e3f91a
--- /dev/null
+++ b/script-beta/src/core/highlight.lua
@@ -0,0 +1,230 @@
+local guide = require 'parser.guide'
+local files = require 'files'
+local vm = require 'vm'
+local define = require 'proto.define'
+
+local function ofLocal(source, callback)
+ callback(source)
+ if source.ref then
+ for _, ref in ipairs(source.ref) do
+ callback(ref)
+ end
+ end
+end
+
+local function ofField(source, uri, callback)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ local myKey = guide.getKeyName(source)
+ if parent.type == 'tableindex'
+ or parent.type == 'tablefield' then
+ local tbl = parent.parent
+ vm.eachField(tbl, function (info)
+ if info.key ~= myKey then
+ return
+ end
+ local destUri = guide.getRoot(info.source).uri
+ if destUri ~= uri then
+ return
+ end
+ callback(info.source)
+ end)
+ else
+ vm.eachField(parent.node, function (info)
+ if info.key ~= myKey then
+ return
+ end
+ local destUri = guide.getRoot(info.source).uri
+ if destUri ~= uri then
+ return
+ end
+ callback(info.source)
+ end)
+ end
+end
+
+local function ofIndex(source, uri, callback)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ if parent.type == 'setindex'
+ or parent.type == 'getindex'
+ or parent.type == 'tableindex' then
+ ofField(source, uri, callback)
+ end
+end
+
+local function ofLabel(source, callback)
+ vm.eachRef(source, function (info)
+ callback(info.source)
+ end)
+end
+
+local function find(source, uri, callback)
+ if source.type == 'local' then
+ ofLocal(source, callback)
+ elseif source.type == 'getlocal'
+ or source.type == 'setlocal' then
+ ofLocal(source.node, callback)
+ elseif source.type == 'field'
+ or source.type == 'method' then
+ ofField(source, uri, callback)
+ elseif source.type == 'string'
+ or source.type == 'boolean'
+ or source.type == 'number' then
+ ofIndex(source, uri, callback)
+ callback(source)
+ elseif source.type == 'nil' then
+ callback(source)
+ elseif source.type == 'goto'
+ or source.type == 'label' then
+ ofLabel(source, callback)
+ end
+end
+
+local function checkInIf(source, text, offset)
+ -- 检查 end
+ local endA = source.finish - #'end' + 1
+ local endB = source.finish
+ if offset >= endA
+ and offset <= endB
+ and text:sub(endA, endB) == 'end' then
+ return true
+ end
+ -- 检查每个子模块
+ for _, block in ipairs(source) do
+ for i = 1, #block.keyword, 2 do
+ local start = block.keyword[i]
+ local finish = block.keyword[i+1]
+ if offset >= start and offset <= finish then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+local function makeIf(source, text, callback)
+ -- end
+ local endA = source.finish - #'end' + 1
+ local endB = source.finish
+ if text:sub(endA, endB) == 'end' then
+ callback(endA, endB)
+ end
+ -- 每个子模块
+ for _, block in ipairs(source) do
+ for i = 1, #block.keyword, 2 do
+ local start = block.keyword[i]
+ local finish = block.keyword[i+1]
+ callback(start, finish)
+ end
+ end
+ return false
+end
+
+local function findKeyword(source, text, offset, callback)
+ if source.type == 'do'
+ or source.type == 'function'
+ or source.type == 'loop'
+ or source.type == 'in'
+ or source.type == 'while'
+ or source.type == 'repeat' then
+ local ok
+ for i = 1, #source.keyword, 2 do
+ local start = source.keyword[i]
+ local finish = source.keyword[i+1]
+ if offset >= start and offset <= finish then
+ ok = true
+ break
+ end
+ end
+ if ok then
+ for i = 1, #source.keyword, 2 do
+ local start = source.keyword[i]
+ local finish = source.keyword[i+1]
+ callback(start, finish)
+ end
+ end
+ elseif source.type == 'if' then
+ local ok = checkInIf(source, text, offset)
+ if ok then
+ makeIf(source, text, callback)
+ end
+ end
+end
+
+return function (uri, offset)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local text = files.getText(uri)
+ local results = {}
+ local mark = {}
+ guide.eachSourceContain(ast.ast, offset, function (source)
+ find(source, uri, function (target)
+ local kind
+ if target.type == 'getfield' then
+ target = target.field
+ kind = define.DocumentHighlightKind.Read
+ elseif target.type == 'setfield'
+ or target.type == 'tablefield' then
+ target = target.field
+ kind = define.DocumentHighlightKind.Write
+ elseif target.type == 'getmethod' then
+ target = target.method
+ kind = define.DocumentHighlightKind.Read
+ elseif target.type == 'setmethod' then
+ target = target.method
+ kind = define.DocumentHighlightKind.Write
+ elseif target.type == 'getindex' then
+ target = target.index
+ kind = define.DocumentHighlightKind.Read
+ elseif target.type == 'setindex'
+ or target.type == 'tableindex' then
+ target = target.index
+ kind = define.DocumentHighlightKind.Write
+ elseif target.type == 'getlocal'
+ or target.type == 'getglobal'
+ or target.type == 'goto' then
+ kind = define.DocumentHighlightKind.Read
+ elseif target.type == 'setlocal'
+ or target.type == 'local'
+ or target.type == 'setglobal'
+ or target.type == 'label' then
+ kind = define.DocumentHighlightKind.Write
+ elseif target.type == 'string'
+ or target.type == 'boolean'
+ or target.type == 'number'
+ or target.type == 'nil' then
+ kind = define.DocumentHighlightKind.Text
+ else
+ log.warn('Unknow target.type:', target.type)
+ return
+ end
+ if mark[target] then
+ return
+ end
+ mark[target] = true
+ results[#results+1] = {
+ start = target.start,
+ finish = target.finish,
+ kind = kind,
+ }
+ end)
+ findKeyword(source, text, offset, function (start, finish)
+ results[#results+1] = {
+ start = start,
+ finish = finish,
+ kind = define.DocumentHighlightKind.Write
+ }
+ end)
+ end)
+ if #results == 0 then
+ return nil
+ end
+ return results
+end
diff --git a/script-beta/src/core/hover/arg.lua b/script-beta/src/core/hover/arg.lua
new file mode 100644
index 00000000..be344488
--- /dev/null
+++ b/script-beta/src/core/hover/arg.lua
@@ -0,0 +1,20 @@
+local guide = require 'parser.guide'
+local vm = require 'vm'
+
+local function asFunction(source)
+ if not source.args then
+ return ''
+ end
+ local args = {}
+ for i = 1, #source.args do
+ local arg = source.args[i]
+ args[i] = ('%s: %s'):format(guide.getName(arg), vm.getType(arg))
+ end
+ return table.concat(args, ', ')
+end
+
+return function (source)
+ if source.type == 'function' then
+ return asFunction(source)
+ end
+end
diff --git a/script-beta/src/core/hover/init.lua b/script-beta/src/core/hover/init.lua
new file mode 100644
index 00000000..b99c14b2
--- /dev/null
+++ b/script-beta/src/core/hover/init.lua
@@ -0,0 +1,56 @@
+local files = require 'files'
+local guide = require 'parser.guide'
+local vm = require 'vm'
+local getLabel = require 'core.hover.label'
+
+local function getHoverAsFunction(source)
+ local values = vm.getValue(source)
+ local labels = {}
+ for _, value in ipairs(values) do
+ if value.type == 'function' then
+ labels[#labels+1] = getLabel(value.source)
+ end
+ end
+
+ local label = table.concat(labels, '\n')
+ return {
+ label = label,
+ source = source,
+ }
+end
+
+local function getHoverAsValue(source)
+ local label = getLabel(source)
+ return {
+ label = label,
+ source = source,
+ }
+end
+
+local function getHover(source)
+ local isFunction = vm.hasType(source, 'function')
+ if isFunction then
+ return getHoverAsFunction(source)
+ else
+ return getHoverAsValue(source)
+ end
+end
+
+return function (uri, offset)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local hover = guide.eachSourceContain(ast.ast, offset, function (source)
+ if source.type == 'local'
+ or source.type == 'setlocal'
+ or source.type == 'getlocal'
+ or source.type == 'setglobal'
+ or source.type == 'getglobal'
+ or source.type == 'field'
+ or source.type == 'method' then
+ return getHover(source)
+ end
+ end)
+ return hover
+end
diff --git a/script-beta/src/core/hover/label.lua b/script-beta/src/core/hover/label.lua
new file mode 100644
index 00000000..72ce60f4
--- /dev/null
+++ b/script-beta/src/core/hover/label.lua
@@ -0,0 +1,103 @@
+local buildName = require 'core.hover.name'
+local buildArg = require 'core.hover.arg'
+local buildReturn = require 'core.hover.return'
+local buildTable = require 'core.hover.table'
+local vm = require 'vm'
+local util = require 'utility'
+
+local function asFunction(source)
+ local name = buildName(source)
+ local arg = buildArg(source)
+ local rtn = buildReturn(source)
+ local lines = {}
+ lines[1] = ('function %s(%s)'):format(name, arg)
+ lines[2] = rtn
+ return table.concat(lines, '\n')
+end
+
+local function asLocal(source)
+ local name = buildName(source)
+ local type = vm.getType(source)
+ local literal = vm.getLiteral(source)
+ if type == 'table' then
+ type = buildTable(source)
+ end
+ if literal == nil then
+ return ('local %s: %s'):format(name, type)
+ else
+ return ('local %s: %s = %s'):format(name, type, util.viewLiteral(literal))
+ end
+end
+
+local function asGlobal(source)
+ local name = buildName(source)
+ local type = vm.getType(source)
+ local literal = vm.getLiteral(source)
+ if type == 'table' then
+ type = buildTable(source)
+ end
+ if literal == nil then
+ return ('global %s: %s'):format(name, type)
+ else
+ return ('global %s: %s = %s'):format(name, type, util.viewLiteral(literal))
+ end
+end
+
+local function isGlobalField(source)
+ if source.type == 'field'
+ or source.type == 'method' then
+ source = source.parent
+ end
+ if source.type == 'setfield'
+ or source.type == 'getfield'
+ or source.type == 'setmethod'
+ or source.type == 'getmethod'
+ or source.type == 'tablefield' then
+ local node = source.node
+ if node.type == 'setglobal'
+ or node.type == 'getglobal' then
+ return true
+ end
+ return isGlobalField(node)
+ else
+ return false
+ end
+end
+
+local function asField(source)
+ if isGlobalField(source) then
+ return asGlobal(source)
+ end
+ local name = buildName(source)
+ local type = vm.getType(source)
+ local literal = vm.getLiteral(source)
+ if type == 'table' then
+ type = buildTable(source)
+ end
+ if literal == nil then
+ return ('field %s: %s'):format(name, type)
+ else
+ return ('field %s: %s = %s'):format(name, type, util.viewLiteral(literal))
+ end
+end
+
+return function (source)
+ if source.type == 'function' then
+ return asFunction(source)
+ elseif source.type == 'local'
+ or source.type == 'getlocal'
+ or source.type == 'setlocal' then
+ return asLocal(source)
+ elseif source.type == 'setglobal'
+ or source.type == 'getglobal' then
+ return asGlobal(source)
+ elseif source.type == 'getfield'
+ or source.type == 'setfield'
+ or source.type == 'getmethod'
+ or source.type == 'setmethod'
+ or source.type == 'tablefield'
+ or source.type == 'field'
+ or source.type == 'method' then
+ return asField(source)
+ end
+end
diff --git a/script-beta/src/core/hover/name.lua b/script-beta/src/core/hover/name.lua
new file mode 100644
index 00000000..a22a8b5a
--- /dev/null
+++ b/script-beta/src/core/hover/name.lua
@@ -0,0 +1,64 @@
+local guide = require 'parser.guide'
+local vm = require 'vm'
+
+local function asLocal(source)
+ return guide.getName(source)
+end
+
+local function asMethod(source)
+ local class = vm.eachField(source.node, function (info)
+ if info.key == 's|type' or info.key == 's|__name' or info.key == 's|name' then
+ if info.value and info.value.type == 'string' then
+ return info.value[1]
+ end
+ end
+ end)
+ local node = class or guide.getName(source.node) or '?'
+ local method = guide.getName(source)
+ return ('%s:%s'):format(node, method)
+end
+
+local function asField(source)
+ local class = vm.eachField(source.node, function (info)
+ if info.key == 's|type' or info.key == 's|__name' or info.key == 's|name' then
+ if info.value and info.value.type == 'string' then
+ return info.value[1]
+ end
+ end
+ end)
+ local node = class or guide.getName(source.node) or '?'
+ local method = guide.getName(source)
+ return ('%s.%s'):format(node, method)
+end
+
+local function asGlobal(source)
+ return guide.getName(source)
+end
+
+local function buildName(source)
+ if source.type == 'local'
+ or source.type == 'getlocal'
+ or source.type == 'setlocal' then
+ return asLocal(source) or ''
+ end
+ if source.type == 'setglobal'
+ or source.type == 'getglobal' then
+ return asGlobal(source) or ''
+ end
+ if source.type == 'setmethod'
+ or source.type == 'getmethod' then
+ return asMethod(source) or ''
+ end
+ if source.type == 'setfield'
+ or source.tyoe == 'getfield'
+ or source.type == 'tablefield' then
+ return asField(source) or ''
+ end
+ local parent = source.parent
+ if parent then
+ return buildName(parent)
+ end
+ return ''
+end
+
+return buildName
diff --git a/script-beta/src/core/hover/return.lua b/script-beta/src/core/hover/return.lua
new file mode 100644
index 00000000..c22626a6
--- /dev/null
+++ b/script-beta/src/core/hover/return.lua
@@ -0,0 +1,34 @@
+local guide = require 'parser.guide'
+local vm = require 'vm'
+
+local function asFunction(source)
+ if not source.returns then
+ return nil
+ end
+ local returns = {}
+ for _, rtn in ipairs(source.returns) do
+ for i = 1, #rtn do
+ local values = vm.getValue(rtn[i])
+ returns[#returns+1] = values
+ end
+ break
+ end
+ if #returns == 0 then
+ return nil
+ end
+ local lines = {}
+ for i = 1, #returns do
+ if i == 1 then
+ lines[i] = (' -> %s'):format(vm.viewType(returns[i]))
+ else
+ lines[i] = ('% 3d. %s'):format(i, returns[i])
+ end
+ end
+ return table.concat(lines, '\n')
+end
+
+return function (source)
+ if source.type == 'function' then
+ return asFunction(source)
+ end
+end
diff --git a/script-beta/src/core/hover/table.lua b/script-beta/src/core/hover/table.lua
new file mode 100644
index 00000000..9ed86692
--- /dev/null
+++ b/script-beta/src/core/hover/table.lua
@@ -0,0 +1,35 @@
+local vm = require 'vm'
+
+local function checkClass(source)
+end
+
+return function (source)
+ local fields = {}
+ local class
+ vm.eachField(source, function (info)
+ if info.key == 's|type' or info.key == 's|__name' or info.key == 's|name' then
+ if info.value and info.value.type == 'string' then
+ class = info.value[1]
+ end
+ end
+ local type = vm.getType(info.source)
+ fields[#fields+1] = ('%s'):format(type)
+ end)
+ local fieldsBuf
+ if #fields == 0 then
+ fieldsBuf = '{}'
+ else
+ local lines = {}
+ lines[#lines+1] = '{'
+ for _, field in ipairs(fields) do
+ lines[#lines+1] = ' ' .. field
+ end
+ lines[#lines+1] = '}'
+ fieldsBuf = table.concat(lines, '\n')
+ end
+ if class then
+ return ('%s %s'):format(class, fieldsBuf)
+ else
+ return fieldsBuf
+ end
+end
diff --git a/script-beta/src/core/reference.lua b/script-beta/src/core/reference.lua
new file mode 100644
index 00000000..7e265e97
--- /dev/null
+++ b/script-beta/src/core/reference.lua
@@ -0,0 +1,84 @@
+local guide = require 'parser.guide'
+local files = require 'files'
+local vm = require 'vm'
+
+local function isFunction(source, offset)
+ if source.type ~= 'function' then
+ return false
+ end
+ -- 必须点在 `function` 这个单词上才能查找函数引用
+ return offset >= source.start and offset < source.start + #'function'
+end
+
+local function findRef(source, offset, callback)
+ if source.type ~= 'local'
+ and source.type ~= 'getlocal'
+ and source.type ~= 'setlocal'
+ and source.type ~= 'setglobal'
+ and source.type ~= 'getglobal'
+ and source.type ~= 'field'
+ and source.type ~= 'tablefield'
+ and source.type ~= 'method'
+ and source.type ~= 'string'
+ and source.type ~= 'number'
+ and source.type ~= 'boolean'
+ and source.type ~= 'goto'
+ and source.type ~= 'label'
+ and not isFunction(source, offset) then
+ return
+ end
+ vm.eachRef(source, function (info)
+ if info.mode == 'declare'
+ or info.mode == 'set'
+ or info.mode == 'get'
+ or info.mode == 'return' then
+ local src = info.source
+ local root = guide.getRoot(src)
+ local uri = root.uri
+ if src.type == 'setfield'
+ or src.type == 'getfield'
+ or src.type == 'tablefield' then
+ callback(src.field, uri)
+ elseif src.type == 'setindex'
+ or src.type == 'getindex'
+ or src.type == 'tableindex' then
+ callback(src.index, uri)
+ elseif src.type == 'getmethod'
+ or src.type == 'setmethod' then
+ callback(src.method, uri)
+ else
+ callback(src, uri)
+ end
+ end
+ if info.mode == 'value' then
+ local src = info.source
+ local root = guide.getRoot(src)
+ local uri = root.uri
+ if src.type == 'function' then
+ if src.parent.type == 'return' then
+ callback(src, uri)
+ end
+ end
+ end
+ end)
+end
+
+return function (uri, offset)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local results = {}
+ guide.eachSourceContain(ast.ast, offset, function (source)
+ findRef(source, offset, function (target, uri)
+ results[#results+1] = {
+ target = target,
+ uri = files.getOriginUri(uri),
+ }
+ end)
+ end)
+ if #results == 0 then
+ return nil
+ end
+ return results
+end
diff --git a/script-beta/src/core/rename.lua b/script-beta/src/core/rename.lua
new file mode 100644
index 00000000..3e4512da
--- /dev/null
+++ b/script-beta/src/core/rename.lua
@@ -0,0 +1,374 @@
+local files = require 'files'
+local vm = require 'vm'
+local guide = require 'parser.guide'
+local proto = require 'proto'
+local define = require 'proto.define'
+local util = require 'utility'
+
+local Forcing
+
+local function askForcing(str)
+ if TEST then
+ return true
+ end
+ if Forcing == false then
+ return false
+ end
+ local version = files.globalVersion
+ -- TODO
+ local item = proto.awaitRequest('window/showMessageRequest', {
+ type = define.MessageType.Warning,
+ message = ('[%s]不是有效的标识符,是否强制替换?'):format(str),
+ actions = {
+ {
+ title = '强制替换',
+ },
+ {
+ title = '取消',
+ },
+ }
+ })
+ if version ~= files.globalVersion then
+ Forcing = false
+ proto.notify('window/showMessage', {
+ type = define.MessageType.Warning,
+ message = '文件发生了变化,替换取消。'
+ })
+ return false
+ end
+ if not item then
+ Forcing = false
+ return false
+ end
+ if item.title == '强制替换' then
+ Forcing = true
+ return true
+ else
+ Forcing = false
+ return false
+ end
+end
+
+local function askForMultiChange(results, newname)
+ if TEST then
+ return true
+ end
+ local uris = {}
+ for _, result in ipairs(results) do
+ local uri = result.uri
+ if not uris[uri] then
+ uris[uri] = 0
+ uris[#uris+1] = uri
+ end
+ uris[uri] = uris[uri] + 1
+ end
+ if #uris <= 1 then
+ return true
+ end
+
+ local version = files.globalVersion
+ -- TODO
+ local item = proto.awaitRequest('window/showMessageRequest', {
+ type = define.MessageType.Warning,
+ message = ('将修改 %d 个文件,共 %d 处。'):format(
+ #uris,
+ #results
+ ),
+ actions = {
+ {
+ title = '继续',
+ },
+ {
+ title = '放弃',
+ },
+ }
+ })
+ if version ~= files.globalVersion then
+ proto.notify('window/showMessage', {
+ type = define.MessageType.Warning,
+ message = '文件发生了变化,替换取消。'
+ })
+ return false
+ end
+ if item and item.title == '继续' then
+ local fileList = {}
+ for _, uri in ipairs(uris) do
+ fileList[#fileList+1] = ('%s (%d)'):format(uri, uris[uri])
+ end
+
+ log.debug(('Renamed [%s]\r\n%s'):format(newname, table.concat(fileList, '\r\n')))
+ return true
+ end
+ return false
+end
+
+local function trim(str)
+ return str:match '^%s*(%S+)%s*$'
+end
+
+local function isValidName(str)
+ return str:match '^[%a_][%w_]*$'
+end
+
+local function isValidGlobal(str)
+ for s in str:gmatch '[^%.]*' do
+ if not isValidName(trim(s)) then
+ return false
+ end
+ end
+ return true
+end
+
+local function isValidFunctionName(str)
+ if isValidGlobal(str) then
+ return true
+ end
+ local pos = str:find(':', 1, true)
+ if not pos then
+ return false
+ end
+ return isValidGlobal(trim(str:sub(1, pos-1)))
+ and isValidName(trim(str:sub(pos+1)))
+end
+
+local function isFunctionGlobalName(source)
+ local parent = source.parent
+ if parent.type ~= 'setglobal' then
+ return false
+ end
+ local value = parent.value
+ if not value.type ~= 'function' then
+ return false
+ end
+ return value.start <= parent.start
+end
+
+local function renameLocal(source, newname, callback)
+ if isValidName(newname) then
+ callback(source, source.start, source.finish, newname)
+ return
+ end
+ if askForcing(newname) then
+ callback(source, source.start, source.finish, newname)
+ end
+end
+
+local function renameField(source, newname, callback)
+ if isValidName(newname) then
+ callback(source, source.start, source.finish, newname)
+ return true
+ end
+ local parent = source.parent
+ if parent.type == 'setfield'
+ or parent.type == 'getfield' then
+ local dot = parent.dot
+ local newstr = '[' .. util.viewString(newname) .. ']'
+ callback(source, dot.start, source.finish, newstr)
+ elseif parent.type == 'tablefield' then
+ local newstr = '[' .. util.viewString(newname) .. ']'
+ callback(source, source.start, source.finish, newstr)
+ elseif parent.type == 'getmethod' then
+ if not askForcing(newname) then
+ return false
+ end
+ callback(source, source.start, source.finish, newname)
+ elseif parent.type == 'setmethod' then
+ local uri = guide.getRoot(source).uri
+ local text = files.getText(uri)
+ local func = parent.value
+ -- function mt:name () end --> mt['newname'] = function (self) end
+ local newstr = string.format('%s[%s] = function '
+ , text:sub(parent.start, parent.node.finish)
+ , util.viewString(newname)
+ )
+ callback(source, func.start, parent.finish, newstr)
+ local pl = text:find('(', parent.finish, true)
+ if pl then
+ if func.args then
+ callback(source, pl + 1, pl, 'self, ')
+ else
+ callback(source, pl + 1, pl, 'self')
+ end
+ end
+ end
+ return true
+end
+
+local function renameGlobal(source, newname, callback)
+ if isValidGlobal(newname) then
+ callback(source, source.start, source.finish, newname)
+ return true
+ end
+ if isValidFunctionName(newname) then
+ if not isFunctionGlobalName(source) then
+ askForcing(newname)
+ end
+ callback(source, source.start, source.finish, newname)
+ return true
+ end
+ local newstr = '_ENV[' .. util.viewString(newname) .. ']'
+ -- function name () end --> _ENV['newname'] = function () end
+ if source.value and source.value.type == 'function'
+ and source.value.start < source.start then
+ callback(source, source.value.start, source.finish, newstr .. ' = function ')
+ return true
+ end
+ callback(source, source.start, source.finish, newstr)
+ return true
+end
+
+local function ofLocal(source, newname, callback)
+ renameLocal(source, newname, callback)
+ if source.ref then
+ for _, ref in ipairs(source.ref) do
+ renameLocal(ref, newname, callback)
+ end
+ end
+end
+
+local function ofField(source, newname, callback)
+ return vm.eachRef(source, function (info)
+ local src = info.source
+ if src.type == 'tablefield'
+ or src.type == 'getfield'
+ or src.type == 'setfield' then
+ src = src.field
+ elseif src.type == 'tableindex'
+ or src.type == 'getindex'
+ or src.type == 'setindex' then
+ src = src.index
+ elseif src.type == 'getmethod'
+ or src.type == 'setmethod' then
+ src = src.method
+ end
+ if src.type == 'string' then
+ local quo = src[2]
+ local text = util.viewString(newname, quo)
+ callback(src, src.start, src.finish, text)
+ return
+ elseif src.type == 'field'
+ or src.type == 'method' then
+ local suc = renameField(src, newname, callback)
+ if not suc then
+ return false
+ end
+ elseif src.type == 'setglobal'
+ or src.type == 'getglobal' then
+ local suc = renameGlobal(src, newname, callback)
+ if not suc then
+ return false
+ end
+ end
+ end)
+end
+
+local function rename(source, newname, callback)
+ if source.type == 'label'
+ or source.type == 'goto' then
+ if not isValidName(newname) and not askForcing(newname)then
+ return false
+ end
+ vm.eachRef(source, function (info)
+ callback(info.source, info.source.start, info.source.finish, newname)
+ end)
+ elseif source.type == 'local' then
+ return ofLocal(source, newname, callback)
+ elseif source.type == 'setlocal'
+ or source.type == 'getlocal' then
+ return ofLocal(source.node, newname, callback)
+ elseif source.type == 'field'
+ or source.type == 'method'
+ or source.type == 'tablefield'
+ or source.type == 'string'
+ or source.type == 'setglobal'
+ or source.type == 'getglobal' then
+ return ofField(source, newname, callback)
+ end
+ return true
+end
+
+local function prepareRename(source)
+ if source.type == 'label'
+ or source.type == 'goto'
+ or source.type == 'local'
+ or source.type == 'setlocal'
+ or source.type == 'getlocal'
+ or source.type == 'field'
+ or source.type == 'method'
+ or source.type == 'tablefield'
+ or source.type == 'setglobal'
+ or source.type == 'getglobal' then
+ return source, source[1]
+ elseif source.type == 'string' then
+ local parent = source.parent
+ if not parent then
+ return nil
+ end
+ if parent.type == 'setindex'
+ or parent.type == 'getindex'
+ or parent.type == 'tableindex' then
+ return source, source[1]
+ end
+ return nil
+ end
+ return nil
+end
+
+local m = {}
+
+function m.rename(uri, pos, newname)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+ local results = {}
+
+ guide.eachSourceContain(ast.ast, pos, function(source)
+ rename(source, newname, function (target, start, finish, text)
+ results[#results+1] = {
+ start = start,
+ finish = finish,
+ text = text,
+ uri = guide.getRoot(target).uri,
+ }
+ end)
+ end)
+
+ if Forcing == false then
+ Forcing = nil
+ return nil
+ end
+
+ if #results == 0 then
+ return nil
+ end
+
+ if not askForMultiChange(results, newname) then
+ return nil
+ end
+
+ return results
+end
+
+function m.prepareRename(uri, pos)
+ local ast = files.getAst(uri)
+ if not ast then
+ return nil
+ end
+
+ local result
+ guide.eachSourceContain(ast.ast, pos, function(source)
+ local res, text = prepareRename(source)
+ if res then
+ result = {
+ start = source.start,
+ finish = source.finish,
+ text = text,
+ }
+ end
+ end)
+
+ return result
+end
+
+return m
diff --git a/script-beta/src/define/DiagnosticDefaultSeverity.lua b/script-beta/src/define/DiagnosticDefaultSeverity.lua
new file mode 100644
index 00000000..cc26cab2
--- /dev/null
+++ b/script-beta/src/define/DiagnosticDefaultSeverity.lua
@@ -0,0 +1,21 @@
+return {
+ ['unused-local'] = 'Hint',
+ ['unused-function'] = 'Hint',
+ ['undefined-global'] = 'Warning',
+ ['global-in-nil-env'] = 'Warning',
+ ['unused-label'] = 'Hint',
+ ['unused-vararg'] = 'Hint',
+ ['trailing-space'] = 'Hint',
+ ['redefined-local'] = 'Hint',
+ ['newline-call'] = 'Information',
+ ['redundant-parameter'] = 'Hint',
+ ['ambiguity-1'] = 'Warning',
+ ['lowercase-global'] = 'Information',
+ ['undefined-env-child'] = 'Information',
+ ['duplicate-index'] = 'Warning',
+ ['duplicate-method'] = 'Warning',
+ ['empty-block'] = 'Hint',
+ ['redundant-value'] = 'Hint',
+ ['emmy-lua'] = 'Warning',
+ ['set-const'] = 'Error',
+}
diff --git a/script-beta/src/define/DiagnosticSeverity.lua b/script-beta/src/define/DiagnosticSeverity.lua
new file mode 100644
index 00000000..05bd3659
--- /dev/null
+++ b/script-beta/src/define/DiagnosticSeverity.lua
@@ -0,0 +1,6 @@
+return {
+ Error = 1,
+ Warning = 2,
+ Information = 3,
+ Hint = 4,
+}
diff --git a/script-beta/src/define/ErrorCodes.lua b/script-beta/src/define/ErrorCodes.lua
new file mode 100644
index 00000000..befb5630
--- /dev/null
+++ b/script-beta/src/define/ErrorCodes.lua
@@ -0,0 +1,16 @@
+
+return {
+ -- Defined by JSON RPC
+ ParseError = -32700,
+ InvalidRequest = -32600,
+ MethodNotFound = -32601,
+ InvalidParams = -32602,
+ InternalError = -32603,
+ serverErrorStart = -32099,
+ serverErrorEnd = -32000,
+ ServerNotInitialized = -32002,
+ UnknownErrorCode = -32001,
+
+ -- Defined by the protocol.
+ RequestCancelled = -32800,
+}
diff --git a/script-beta/src/doctor.lua b/script-beta/src/doctor.lua
new file mode 100644
index 00000000..08ec69cf
--- /dev/null
+++ b/script-beta/src/doctor.lua
@@ -0,0 +1,380 @@
+local type = type
+local next = next
+local ipairs = ipairs
+local rawget = rawget
+local pcall = pcall
+local getregistry = debug.getregistry
+local getmetatable = debug.getmetatable
+local getupvalue = debug.getupvalue
+local getuservalue = debug.getuservalue
+local getlocal = debug.getlocal
+local getinfo = debug.getinfo
+local maxinterger = math.maxinteger
+local mathType = math.type
+local tableConcat = table.concat
+local _G = _G
+local registry = getregistry()
+local tableSort = table.sort
+
+_ENV = nil
+
+local m = {}
+
+local function getTostring(obj)
+ local mt = getmetatable(obj)
+ if not mt then
+ return nil
+ end
+ local toString = rawget(mt, '__tostring')
+ if not toString then
+ return nil
+ end
+ local suc, str = pcall(toString, obj)
+ if not suc then
+ return nil
+ end
+ if type(str) ~= 'string' then
+ return nil
+ end
+ return str
+end
+
+local function formatName(obj)
+ local tp = type(obj)
+ if tp == 'nil' then
+ return 'nil:nil'
+ elseif tp == 'boolean' then
+ if obj == true then
+ return 'boolean:true'
+ else
+ return 'boolean:false'
+ end
+ elseif tp == 'number' then
+ if mathType(obj) == 'integer' then
+ return ('number:%d'):format(obj)
+ else
+ -- 如果浮点数可以完全表示为整数,那么就转换为整数
+ local str = ('%.10f'):format(obj):gsub('%.?[0]+$', '')
+ if str:find('.', 1, true) then
+ -- 如果浮点数不能表示为整数,那么再加上它的精确表示法
+ str = ('%s(%q)'):format(str, obj)
+ end
+ return 'number:' .. str
+ end
+ elseif tp == 'string' then
+ local str = ('%q'):format(obj)
+ if #str > 100 then
+ local new = ('%s...(len=%d)'):format(str:sub(1, 100), #str)
+ if #new < #str then
+ str = new
+ end
+ end
+ return 'string:' .. str
+ elseif tp == 'function' then
+ local info = getinfo(obj, 'S')
+ if info.what == 'c' then
+ return ('function:%p(C)'):format(obj)
+ elseif info.what == 'main' then
+ return ('function:%p(main)'):format(obj)
+ else
+ return ('function:%p(%s:%d-%d)'):format(obj, info.source, info.linedefined, info.lastlinedefined)
+ end
+ elseif tp == 'table' then
+ local id = getTostring(obj)
+ if not id then
+ if obj == _G then
+ id = '_G'
+ elseif obj == registry then
+ id = 'registry'
+ end
+ end
+ if id then
+ return ('table:%p(%s)'):format(obj, id)
+ else
+ return ('table:%p'):format(obj)
+ end
+ elseif tp == 'userdata' then
+ local id = getTostring(obj)
+ if id then
+ return ('userdata:%p(%s)'):format(obj, id)
+ else
+ return ('userdata:%p'):format(obj)
+ end
+ else
+ return ('%s:%p'):format(tp, obj)
+ end
+end
+
+--- 内存快照
+---@return table
+function m.snapshot()
+ local mark = {}
+ local find
+
+ local function findTable(t, result)
+ result = result or {}
+ local mt = getmetatable(t)
+ local wk, wv
+ if mt then
+ local mode = rawget(mt, '__mode')
+ if type(mode) == 'string' then
+ if mode:find('k', 1, true) then
+ wk = true
+ end
+ if mode:find('v', 1, true) then
+ wv = true
+ end
+ end
+ end
+ for k, v in next, t do
+ if not wk then
+ local keyInfo = find(k)
+ if keyInfo then
+ result[#result+1] = {
+ type = 'key',
+ name = formatName(k),
+ info = keyInfo,
+ }
+ end
+ end
+ if not wv then
+ local valueInfo = find(v)
+ if valueInfo then
+ result[#result+1] = {
+ type = 'field',
+ name = formatName(k) .. '|' .. formatName(v),
+ info = valueInfo,
+ }
+ end
+ end
+ end
+ local MTInfo = find(getmetatable(t))
+ if MTInfo then
+ result[#result+1] = {
+ type = 'metatable',
+ name = '',
+ info = MTInfo,
+ }
+ end
+ if #result == 0 then
+ return nil
+ end
+ return result
+ end
+
+ local function findFunction(f, result, trd, stack)
+ result = result or {}
+ for i = 1, maxinterger do
+ local n, v = getupvalue(f, i)
+ if not n then
+ break
+ end
+ local valueInfo = find(v)
+ if valueInfo then
+ result[#result+1] = {
+ type = 'upvalue',
+ name = n,
+ info = valueInfo,
+ }
+ end
+ end
+ if trd then
+ for i = 1, maxinterger do
+ local n, l = getlocal(trd, stack, i)
+ if not n then
+ break
+ end
+ local valueInfo = find(l)
+ if valueInfo then
+ result[#result+1] = {
+ type = 'local',
+ name = n,
+ info = valueInfo,
+ }
+ end
+ end
+ end
+ if #result == 0 then
+ return nil
+ end
+ return result
+ end
+
+ local function findUserData(u, result)
+ result = result or {}
+ for i = 1, maxinterger do
+ local v, b = getuservalue(u, i)
+ if not b then
+ break
+ end
+ local valueInfo = find(v)
+ if valueInfo then
+ result[#result+1] = {
+ type = 'uservalue',
+ name = formatName(i),
+ info = valueInfo,
+ }
+ end
+ end
+ local MTInfo = find(getmetatable(u))
+ if MTInfo then
+ result[#result+1] = {
+ type = 'metatable',
+ name = '',
+ info = MTInfo,
+ }
+ end
+ if #result == 0 then
+ return nil
+ end
+ return result
+ end
+
+ local function findThread(trd, result)
+ -- 不查找主线程,主线程一定是临时的(视为弱引用)
+ if trd == registry[1] then
+ return nil
+ end
+ result = result or {}
+
+ for i = 1, maxinterger do
+ local info = getinfo(trd, i, 'Sf')
+ if not info then
+ break
+ end
+ local funcInfo = find(info.func, trd, i)
+ if funcInfo then
+ result[#result+1] = {
+ type = 'stack',
+ name = i .. '@' .. formatName(info.func),
+ info = funcInfo,
+ }
+ end
+ end
+
+ if #result == 0 then
+ return nil
+ end
+ return result
+ end
+
+ function find(obj, trd, stack)
+ if mark[obj] then
+ return mark[obj]
+ end
+ local tp = type(obj)
+ if tp == 'table' then
+ mark[obj] = {}
+ mark[obj] = findTable(obj, mark[obj])
+ elseif tp == 'function' then
+ mark[obj] = {}
+ mark[obj] = findFunction(obj, mark[obj], trd, stack)
+ elseif tp == 'userdata' then
+ mark[obj] = {}
+ mark[obj] = findUserData(obj, mark[obj])
+ elseif tp == 'thread' then
+ mark[obj] = {}
+ mark[obj] = findThread(obj, mark[obj])
+ else
+ return nil
+ end
+ if mark[obj] then
+ mark[obj].object = obj
+ end
+ return mark[obj]
+ end
+
+ return {
+ name = formatName(registry),
+ type = 'root',
+ info = find(registry),
+ }
+end
+
+--- 寻找对象的引用
+---@return string
+function m.catch(...)
+ local targets = {}
+ for _, target in ipairs {...} do
+ targets[target] = true
+ end
+ local report = m.snapshot()
+ local path = {}
+ local result = {}
+ local mark = {}
+
+ local function push()
+ result[#result+1] = tableConcat(path, ' => ')
+ end
+
+ local function search(t)
+ path[#path+1] = ('(%s)%s'):format(t.type, t.name)
+ local addTarget
+ if targets[t.info.object] then
+ targets[t.info.object] = nil
+ addTarget = t.info.object
+ push(t)
+ end
+ if not mark[t.info] then
+ mark[t.info] = true
+ for _, obj in ipairs(t.info) do
+ search(obj)
+ end
+ end
+ path[#path] = nil
+ if addTarget then
+ targets[addTarget] = true
+ end
+ end
+
+ search(report)
+
+ return result
+end
+
+--- 生成一个报告
+---@return string
+function m.report()
+ local snapshot = m.snapshot()
+ local cache = {}
+ local mark = {}
+
+ local function scan(t)
+ local obj = t.info.object
+ local tp = type(obj)
+ if tp == 'table'
+ or tp == 'userdata'
+ or tp == 'function'
+ or tp == 'string'
+ or tp == 'thread' then
+ local point = ('%p'):format(obj)
+ if not cache[point] then
+ cache[point] = {
+ point = point,
+ count = 0,
+ name = formatName(obj),
+ }
+ end
+ cache[point].count = cache[point].count + 1
+ end
+ if not mark[t.info] then
+ mark[t.info] = true
+ for _, child in ipairs(t.info) do
+ scan(child)
+ end
+ end
+ end
+
+ scan(snapshot)
+
+ local list = {}
+ for _, info in next, cache do
+ list[#list+1] = info
+ end
+ tableSort(list, function (a, b)
+ return a.name < b.name
+ end)
+ return list
+end
+
+return m
diff --git a/script-beta/src/file-uri.lua b/script-beta/src/file-uri.lua
new file mode 100644
index 00000000..8acd4f64
--- /dev/null
+++ b/script-beta/src/file-uri.lua
@@ -0,0 +1,108 @@
+local platform = require 'bee.platform'
+
+local esc = {
+ [':'] = '%3A',
+ ['/'] = '%2F',
+ ['?'] = '%3F',
+ ['#'] = '%23',
+ ['['] = '%5B',
+ [']'] = '%5D',
+ ['@'] = '%40',
+
+ ['!'] = '%21', -- sub-delims
+ ['$'] = '%24',
+ ['&'] = '%26',
+ ["'"] = '%27',
+ ['('] = '%28',
+ [')'] = '%29',
+ ['*'] = '%2A',
+ ['+'] = '%2B',
+ [','] = '%2C',
+ [';'] = '%3B',
+ ['='] = '%3D',
+
+ [' '] = '%20',
+}
+
+local escPatt = '[^%w%-%.%_%~%/]'
+
+local function normalize(str)
+ return str:gsub('%%(%x%x)', function (n)
+ return string.char(tonumber(n, 16))
+ end)
+end
+
+local m = {}
+
+-- c:\my\files --> file:///c%3A/my/files
+-- /usr/home --> file:///usr/home
+-- \\server\share\some\path --> file://server/share/some/path
+
+--- path -> uri
+---@param path string
+---@return string uri
+function m.encode(path)
+ local authority = ''
+ if platform.OS == 'Windows' then
+ path = path:gsub('\\', '/')
+ end
+
+ if path:sub(1, 2) == '//' then
+ local idx = path:find('/', 3)
+ if idx then
+ authority = path:sub(3, idx)
+ path = path:sub(idx + 1)
+ if path == '' then
+ path = '/'
+ end
+ else
+ authority = path:sub(3)
+ path = '/'
+ end
+ end
+
+ if path:sub(1, 1) ~= '/' then
+ path = '/' .. path
+ end
+
+ -- lower-case windows drive letters in /C:/fff or C:/fff
+ if path:match '/%u:' then
+ path = path:lower()
+ end
+
+ local uri = 'file://'
+ .. authority:gsub(escPatt, esc)
+ .. path:gsub(escPatt, esc)
+ return uri
+end
+
+-- file:///c%3A/my/files --> c:\my\files
+-- file:///usr/home --> /usr/home
+-- file://server/share/some/path --> \\server\share\some\path
+
+--- uri -> path
+---@param uri string
+---@return string path
+function m.decode(uri)
+ local scheme, authority, path = uri:match('([^:]*):?/?/?([^/]*)(.*)')
+ if not scheme then
+ return ''
+ end
+ scheme = normalize(scheme)
+ authority = normalize(authority)
+ path = normalize(path)
+ local value
+ if scheme == 'file' and #authority > 0 and #path > 1 then
+ value = '//' .. authority .. path
+ elseif path:match '/%a:' then
+ value = path:sub(2, 2):lower() .. path:sub(3)
+ else
+ value = path
+ end
+ if platform.OS == 'Windows' then
+ value = value:gsub('/', '\\')
+ end
+ return value
+end
+
+return m
diff --git a/script-beta/src/files.lua b/script-beta/src/files.lua
new file mode 100644
index 00000000..ac27117c
--- /dev/null
+++ b/script-beta/src/files.lua
@@ -0,0 +1,290 @@
+local platform = require 'bee.platform'
+local config = require 'config'
+local glob = require 'glob'
+local furi = require 'file-uri'
+local parser = require 'parser'
+local vm = require 'vm.vm'
+local guide = require 'parser.guide'
+
+local m = {}
+
+m.openMap = {}
+m.fileMap = {}
+m.assocVersion = -1
+m.assocMatcher = nil
+m.globalVersion = 0
+
+--- 打开文件
+---@param uri string
+function m.open(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ m.openMap[uri] = true
+end
+
+--- 关闭文件
+---@param uri string
+function m.close(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ m.openMap[uri] = nil
+end
+
+--- 是否打开
+---@param uri string
+---@return boolean
+function m.isOpen(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ return m.openMap[uri] == true
+end
+
+--- 是否存在
+---@return boolean
+function m.exists(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ return m.fileMap[uri] ~= nil
+end
+
+--- 设置文件文本
+---@param uri string
+---@param text string
+function m.setText(uri, text)
+ local originUri = uri
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ if not m.fileMap[uri] then
+ m.fileMap[uri] = {
+ uri = originUri,
+ }
+ end
+ local file = m.fileMap[uri]
+ if file.text == text then
+ return
+ end
+ file.text = text
+ file.vm = nil
+ file.lines = nil
+ file.ast = nil
+ file.globals = nil
+ file.links = nil
+ m.globalVersion = m.globalVersion + 1
+ vm.refreshCache()
+
+ local diagnostic = require 'provider.diagnostic'
+ diagnostic.refresh(originUri)
+end
+
+--- 监听编译完成
+function m.onCompiled(uri, callback)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if not file then
+ return
+ end
+ if not file.onCompiledList then
+ file.onCompiledList = {}
+ end
+ file.onCompiledList[#file.onCompiledList+1] = callback
+end
+
+--- 获取文件文本
+---@param uri string
+---@return string text
+function m.getText(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if not file then
+ return nil
+ end
+ return file.text
+end
+
+--- 移除文件
+---@param uri string
+function m.remove(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if not file then
+ return
+ end
+ m.fileMap[uri] = nil
+
+ m.globalVersion = m.globalVersion + 1
+ vm.refreshCache()
+
+ local diagnostic = require 'service.diagnostic'
+ diagnostic.refresh(file.uri)
+ diagnostic.clear(file.uri)
+end
+
+--- 移除所有文件
+function m.removeAll()
+ for uri in pairs(m.fileMap) do
+ m.fileMap[uri] = nil
+ end
+ m.globalVersion = m.globalVersion + 1
+ vm.refreshCache()
+end
+
+--- 遍历文件
+function m.eachFile()
+ return pairs(m.fileMap)
+end
+
+--- 获取文件语法树
+---@param uri string
+---@return table ast
+function m.getAst(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if file.ast == nil then
+ local state, err = parser:compile(file.text, 'lua', config.config.runtime.version)
+ if state then
+ state.uri = file.uri
+ state.ast.uri = file.uri
+ file.ast = state
+ else
+ log.error(err)
+ file.ast = false
+ return nil
+ end
+ end
+ return file.ast
+end
+
+--- 获取文件行信息
+---@param uri string
+---@return table lines
+function m.getLines(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if not file then
+ return nil
+ end
+ if not file.lines then
+ file.lines = parser:lines(file.text)
+ end
+ return file.lines
+end
+
+--- 获取原始uri
+function m.getOriginUri(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local file = m.fileMap[uri]
+ if not file then
+ return nil
+ end
+ return file.uri
+end
+
+--- 寻找全局变量
+function m.findGlobals(name)
+ local uris = {}
+ for uri, file in pairs(m.fileMap) do
+ if not file.globals then
+ file.globals = {}
+ local ast = m.getAst(uri)
+ if ast then
+ local globals = vm.getGlobals(ast.ast)
+ for name in pairs(globals) do
+ file.globals[name] = true
+ end
+ end
+ end
+ if file.globals[name] then
+ uris[#uris+1] = file.uri
+ end
+ end
+ return uris
+end
+
+--- 寻找link自己的其他文件
+function m.findLinkTo(uri)
+ if platform.OS == 'Windows' then
+ uri = uri:lower()
+ end
+ local result = {}
+ for _, file in pairs(m.fileMap) do
+ if file.links == nil then
+ local ast = m.getAst(file.uri)
+ if ast then
+ file.links = vm.getLinks(ast.ast)
+ else
+ file.links = false
+ end
+ end
+ if file.links then
+ for linkUri in pairs(file.links) do
+ if m.eq(uri, linkUri) then
+ result[#result+1] = file.uri
+ end
+ end
+ end
+ end
+ return result
+end
+
+--- 判断文件名相等
+function m.eq(a, b)
+ if platform.OS == 'Windows' then
+ return a:lower() == b:lower()
+ else
+ return a == b
+ end
+end
+
+--- 获取文件关联
+function m.getAssoc()
+ if m.assocVersion == config.version then
+ return m.assocMatcher
+ end
+ m.assocVersion = config.version
+ local patt = {}
+ for k, v in pairs(config.other.associations) do
+ if m.eq(v, 'lua') then
+ patt[#patt+1] = k
+ end
+ end
+ m.assocMatcher = glob.glob(patt)
+ if platform.OS == 'Windows' then
+ m.assocMatcher:setOption 'ignoreCase'
+ end
+ return m.assocMatcher
+end
+
+--- 判断是否是Lua文件
+---@param uri string
+---@return boolean
+function m.isLua(uri)
+ local ext = uri:match '%.([^%.%/%\\]-)$'
+ if not ext then
+ return false
+ end
+ if m.eq(ext, 'lua') then
+ return true
+ end
+ local matcher = m.getAssoc()
+ local path = furi.decode(uri)
+ return matcher(path)
+end
+
+return m
diff --git a/script-beta/src/fs-utility.lua b/script-beta/src/fs-utility.lua
new file mode 100644
index 00000000..14dcb08f
--- /dev/null
+++ b/script-beta/src/fs-utility.lua
@@ -0,0 +1,314 @@
+local fs = require 'bee.filesystem'
+local platform = require 'bee.platform'
+
+local type = type
+local ioOpen = io.open
+local pcall = pcall
+local pairs = pairs
+local setmetatable = setmetatable
+local next = next
+
+_ENV = nil
+
+local m = {}
+--- 读取文件
+---@param path string
+function m.loadFile(path)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local f, e = ioOpen(path, 'rb')
+ if not f then
+ return nil, e
+ end
+ if f:read(3) ~= '\xEF\xBB\xBF' then
+ f:seek("set")
+ end
+ local buf = f:read 'a'
+ f:close()
+ return buf
+end
+
+--- 写入文件
+---@param path string
+---@param content string
+function m.saveFile(path, content)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local f, e = ioOpen(path, "wb")
+
+ if f then
+ f:write(content)
+ f:close()
+ return true
+ else
+ return false, e
+ end
+end
+
+local function buildOptional(optional)
+ optional = optional or {}
+ optional.add = optional.add or {}
+ optional.del = optional.del or {}
+ optional.mod = optional.mod or {}
+ optional.err = optional.err or {}
+ return optional
+end
+
+local function fsAbsolute(path, optional)
+ if type(path) == 'string' then
+ local suc, res = pcall(fs.path, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return nil
+ end
+ path = res
+ end
+ local suc, res = pcall(fs.absolute, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return nil
+ end
+ return res
+end
+
+local function fsIsDirectory(path, optional)
+ local suc, res = pcall(fs.is_directory, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return res
+end
+
+local function fsRemove(path, optional)
+ local suc, res = pcall(fs.remove, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ end
+ optional.del[#optional.del+1] = path:string()
+end
+
+local function fsExists(path, optional)
+ local suc, res = pcall(fs.exists, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return res
+end
+
+local function fsCopy(source, target, optional)
+ local suc, res = pcall(fs.copy_file, source, target, true)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return true
+end
+
+local function fsCreateDirectories(path, optional)
+ local suc, res = pcall(fs.create_directories, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return true
+end
+
+local function fileRemove(path, optional)
+ if optional.onRemove and optional.onRemove(path) == false then
+ return
+ end
+ if fsIsDirectory(path, optional) then
+ for child in path:list_directory() do
+ fileRemove(child, optional)
+ end
+ end
+ if fsRemove(path, optional) then
+ optional.del[#optional.del+1] = path:string()
+ end
+end
+
+local function fileCopy(source, target, optional)
+ local isDir1 = fsIsDirectory(source, optional)
+ local isDir2 = fsIsDirectory(target, optional)
+ local isExists = fsExists(target, optional)
+ if isDir1 then
+ if isDir2 or fsCreateDirectories(target) then
+ for filePath in source:list_directory() do
+ local name = filePath:filename()
+ fileCopy(filePath, target / name, optional)
+ end
+ end
+ else
+ if isExists and not isDir2 then
+ local buf1, err1 = m.loadFile(source)
+ local buf2, err2 = m.loadFile(target)
+ if buf1 and buf2 then
+ if buf1 ~= buf2 then
+ if fsCopy(source, target, optional) then
+ optional.mod[#optional.mod+1] = target:string()
+ end
+ end
+ else
+ if not buf1 then
+ optional.err[#optional.err+1] = err1
+ end
+ if not buf2 then
+ optional.err[#optional.err+1] = err2
+ end
+ end
+ else
+ if fsCopy(source, target, optional) then
+ optional.add[#optional.add+1] = target:string()
+ end
+ end
+ end
+end
+
+local function fileSync(source, target, optional)
+ local isDir1 = fsIsDirectory(source, optional)
+ local isDir2 = fsIsDirectory(target, optional)
+ local isExists = fsExists(target, optional)
+ if isDir1 then
+ if isDir2 then
+ local fileList = m.fileList()
+ for filePath in target:list_directory() do
+ fileList[filePath] = true
+ end
+ for filePath in source:list_directory() do
+ local name = filePath:filename()
+ local targetPath = target / name
+ fileSync(filePath, targetPath, optional)
+ fileList[targetPath] = nil
+ end
+ for path in pairs(fileList) do
+ fileRemove(path, optional)
+ end
+ else
+ if isExists then
+ fileRemove(target, optional)
+ end
+ if fsCreateDirectories(target) then
+ for filePath in source:list_directory() do
+ local name = filePath:filename()
+ fileCopy(filePath, target / name, optional)
+ end
+ end
+ end
+ else
+ if isDir2 then
+ fileRemove(target, optional)
+ end
+ if isExists then
+ local buf1, err1 = m.loadFile(source)
+ local buf2, err2 = m.loadFile(target)
+ if buf1 and buf2 then
+ if buf1 ~= buf2 then
+ if fsCopy(source, target, optional) then
+ optional.mod[#optional.mod+1] = target:string()
+ end
+ end
+ else
+ if not buf1 then
+ optional.err[#optional.err+1] = err1
+ end
+ if not buf2 then
+ optional.err[#optional.err+1] = err2
+ end
+ end
+ else
+ if fsCopy(source, target, optional) then
+ optional.add[#optional.add+1] = target:string()
+ end
+ end
+ end
+end
+
+--- 文件列表
+function m.fileList(optional)
+ optional = optional or buildOptional(optional)
+ local os = platform.OS
+ local keyMap = {}
+ local fileList = {}
+ local function computeKey(path)
+ path = fsAbsolute(path, optional)
+ if not path then
+ return nil
+ end
+ local key
+ if os == 'Windows' then
+ key = path:string():lower()
+ else
+ key = path:string()
+ end
+ return key
+ end
+ return setmetatable({}, {
+ __index = function (_, path)
+ local key = computeKey(path)
+ return fileList[key]
+ end,
+ __newindex = function (_, path, value)
+ local key = computeKey(path)
+ if not key then
+ return
+ end
+ if value == nil then
+ keyMap[key] = nil
+ else
+ keyMap[key] = path
+ fileList[key] = value
+ end
+ end,
+ __pairs = function ()
+ local key, path
+ return function ()
+ key, path = next(keyMap, key)
+ return path, fileList[key]
+ end
+ end,
+ })
+end
+
+--- 删除文件(夹)
+function m.fileRemove(path, optional)
+ optional = buildOptional(optional)
+ path = fsAbsolute(path, optional)
+
+ fileRemove(path, optional)
+
+ return optional
+end
+
+--- 复制文件(夹)
+---@param source string
+---@param target string
+---@return table
+function m.fileCopy(source, target, optional)
+ optional = buildOptional(optional)
+ source = fsAbsolute(source, optional)
+ target = fsAbsolute(target, optional)
+
+ fileCopy(source, target, optional)
+
+ return optional
+end
+
+--- 同步文件(夹)
+---@param source string
+---@param target string
+---@return table
+function m.fileSync(source, target, optional)
+ optional = buildOptional(optional)
+ source = fsAbsolute(source, optional)
+ target = fsAbsolute(target, optional)
+
+ fileSync(source, target, optional)
+
+ return optional
+end
+
+return m
diff --git a/script-beta/src/glob/gitignore.lua b/script-beta/src/glob/gitignore.lua
new file mode 100644
index 00000000..f98a2f31
--- /dev/null
+++ b/script-beta/src/glob/gitignore.lua
@@ -0,0 +1,221 @@
+local m = require 'lpeglabel'
+local matcher = require 'glob.matcher'
+
+local function prop(name, pat)
+ return m.Cg(m.Cc(true), name) * pat
+end
+
+local function object(type, pat)
+ return m.Ct(
+ m.Cg(m.Cc(type), 'type') *
+ m.Cg(pat, 'value')
+ )
+end
+
+local function expect(p, err)
+ return p + m.T(err)
+end
+
+local parser = m.P {
+ 'Main',
+ ['Sp'] = m.S(' \t')^0,
+ ['Slash'] = m.S('/\\')^1,
+ ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}')
+ + m.Ct(m.V'Pattern')
+ + m.T'Main Failed'
+ ,
+ ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"'))
+ + m.Ct(m.V'Unit')
+ ,
+ ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')),
+ ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp',
+ ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp',
+ ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)),
+ ['CSymbol'] = object('*', m.P'*')
+ + object('?', m.P'?')
+ + object('[]', m.V'Range')
+ ,
+ ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1),
+ ['FSymbol'] = object('**', m.P'**'),
+ ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1,
+ ['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1),
+}
+
+local mt = {}
+mt.__index = mt
+mt.__name = 'gitignore'
+
+function mt:addPattern(pat)
+ if type(pat) ~= 'string' then
+ return
+ end
+ self.pattern[#self.pattern+1] = pat
+ if self.options.ignoreCase then
+ pat = pat:lower()
+ end
+ local states, err = parser:match(pat)
+ if not states then
+ self.errors[#self.errors+1] = {
+ pattern = pat,
+ message = err
+ }
+ return
+ end
+ for _, state in ipairs(states) do
+ self.matcher[#self.matcher+1] = matcher(state)
+ end
+end
+
+function mt:setOption(op, val)
+ if val == nil then
+ val = true
+ end
+ self.options[op] = val
+end
+
+---@param key string | "'type'" | "'list'"
+---@param func function | "function (path) end"
+function mt:setInterface(key, func)
+ if type(func) ~= 'function' then
+ return
+ end
+ self.interface[key] = func
+end
+
+function mt:callInterface(name, ...)
+ local func = self.interface[name]
+ return func(...)
+end
+
+function mt:hasInterface(name)
+ return self.interface[name] ~= nil
+end
+
+function mt:checkDirectory(catch, path, matcher)
+ if not self:hasInterface 'type' then
+ return true
+ end
+ if not matcher:isNeedDirectory() then
+ return true
+ end
+ if #catch < #path then
+ -- if path is 'a/b/c' and catch is 'a/b'
+ -- then the catch must be a directory
+ return true
+ else
+ return self:callInterface('type', path) == 'directory'
+ end
+end
+
+function mt:simpleMatch(path)
+ for i = #self.matcher, 1, -1 do
+ local matcher = self.matcher[i]
+ local catch = matcher(path)
+ if catch and self:checkDirectory(catch, path, matcher) then
+ if matcher:isNegative() then
+ return false
+ else
+ return true
+ end
+ end
+ end
+ return nil
+end
+
+function mt:finishMatch(path)
+ local paths = {}
+ for filename in path:gmatch '[^/\\]+' do
+ paths[#paths+1] = filename
+ end
+ for i = 1, #paths do
+ local newPath = table.concat(paths, '/', 1, i)
+ local passed = self:simpleMatch(newPath)
+ if passed == true then
+ return true
+ elseif passed == false then
+ return false
+ end
+ end
+ return false
+end
+
+function mt:scan(callback)
+ local files = {}
+ if type(callback) ~= 'function' then
+ callback = nil
+ end
+ local list = {}
+ local result = self:callInterface('list', '')
+ if type(result) ~= 'table' then
+ return files
+ end
+ for _, path in ipairs(result) do
+ list[#list+1] = path:match '([^/\\]+)[/\\]*$'
+ end
+ while #list > 0 do
+ local current = list[#list]
+ if not current then
+ break
+ end
+ list[#list] = nil
+ if not self:simpleMatch(current) then
+ local fileType = self:callInterface('type', current)
+ if fileType == 'file' then
+ if callback then
+ callback(current)
+ end
+ files[#files+1] = current
+ elseif fileType == 'directory' then
+ local result = self:callInterface('list', current)
+ if type(result) == 'table' then
+ for _, path in ipairs(result) do
+ local filename = path:match '([^/\\]+)[/\\]*$'
+ if filename then
+ list[#list+1] = current .. '/' .. filename
+ end
+ end
+ end
+ end
+ end
+ end
+ return files
+end
+
+function mt:__call(path)
+ if self.options.ignoreCase then
+ path = path:lower()
+ end
+ return self:finishMatch(path)
+end
+
+return function (pattern, options, interface)
+ local self = setmetatable({
+ pattern = {},
+ options = {},
+ matcher = {},
+ errors = {},
+ interface = {},
+ }, mt)
+
+ if type(pattern) == 'table' then
+ for _, pat in ipairs(pattern) do
+ self:addPattern(pat)
+ end
+ else
+ self:addPattern(pattern)
+ end
+
+ if type(options) == 'table' then
+ for op, val in pairs(options) do
+ self:setOption(op, val)
+ end
+ end
+
+ if type(interface) == 'table' then
+ for key, func in pairs(interface) do
+ self:setInterface(key, func)
+ end
+ end
+
+ return self
+end
diff --git a/script-beta/src/glob/glob.lua b/script-beta/src/glob/glob.lua
new file mode 100644
index 00000000..aa8923f3
--- /dev/null
+++ b/script-beta/src/glob/glob.lua
@@ -0,0 +1,122 @@
+local m = require 'lpeglabel'
+local matcher = require 'glob.matcher'
+
+local function prop(name, pat)
+ return m.Cg(m.Cc(true), name) * pat
+end
+
+local function object(type, pat)
+ return m.Ct(
+ m.Cg(m.Cc(type), 'type') *
+ m.Cg(pat, 'value')
+ )
+end
+
+local function expect(p, err)
+ return p + m.T(err)
+end
+
+local parser = m.P {
+ 'Main',
+ ['Sp'] = m.S(' \t')^0,
+ ['Slash'] = m.S('/\\')^1,
+ ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}')
+ + m.Ct(m.V'Pattern')
+ + m.T'Main Failed'
+ ,
+ ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"'))
+ + m.Ct(m.V'Unit')
+ ,
+ ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')),
+ ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp',
+ ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp',
+ ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)),
+ ['CSymbol'] = object('*', m.P'*')
+ + object('?', m.P'?')
+ + object('[]', m.V'Range')
+ ,
+ ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1),
+ ['FSymbol'] = object('**', m.P'**'),
+ ['RangeWord'] = 1 - m.P']',
+ ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1,
+ ['RangeUnit'] = m.Ct(m.C(m.V'RangeWord') * m.P'-' * m.C(m.V'RangeWord'))
+ + m.V'RangeWord',
+}
+
+local mt = {}
+mt.__index = mt
+mt.__name = 'glob'
+
+function mt:addPattern(pat)
+ if type(pat) ~= 'string' then
+ return
+ end
+ self.pattern[#self.pattern+1] = pat
+ if self.options.ignoreCase then
+ pat = pat:lower()
+ end
+ local states, err = parser:match(pat)
+ if not states then
+ self.errors[#self.errors+1] = {
+ pattern = pat,
+ message = err
+ }
+ return
+ end
+ for _, state in ipairs(states) do
+ if state.neg then
+ self.refused[#self.refused+1] = matcher(state)
+ else
+ self.passed[#self.passed+1] = matcher(state)
+ end
+ end
+end
+
+function mt:setOption(op, val)
+ if val == nil then
+ val = true
+ end
+ self.options[op] = val
+end
+
+function mt:__call(path)
+ if self.options.ignoreCase then
+ path = path:lower()
+ end
+ for _, refused in ipairs(self.refused) do
+ if refused(path) then
+ return false
+ end
+ end
+ for _, passed in ipairs(self.passed) do
+ if passed(path) then
+ return true
+ end
+ end
+ return false
+end
+
+return function (pattern, options)
+ local self = setmetatable({
+ pattern = {},
+ options = {},
+ passed = {},
+ refused = {},
+ errors = {},
+ }, mt)
+
+ if type(pattern) == 'table' then
+ for _, pat in ipairs(pattern) do
+ self:addPattern(pat)
+ end
+ else
+ self:addPattern(pattern)
+ end
+
+ if type(options) == 'table' then
+ for op, val in pairs(options) do
+ self:setOption(op, val)
+ end
+ end
+ return self
+end
diff --git a/script-beta/src/glob/init.lua b/script-beta/src/glob/init.lua
new file mode 100644
index 00000000..6578a0d4
--- /dev/null
+++ b/script-beta/src/glob/init.lua
@@ -0,0 +1,4 @@
+return {
+ glob = require 'glob.glob',
+ gitignore = require 'glob.gitignore',
+}
diff --git a/script-beta/src/glob/matcher.lua b/script-beta/src/glob/matcher.lua
new file mode 100644
index 00000000..f4c2b12c
--- /dev/null
+++ b/script-beta/src/glob/matcher.lua
@@ -0,0 +1,151 @@
+local m = require 'lpeglabel'
+
+local Slash = m.S('/\\')^1
+local Symbol = m.S',{}[]*?/\\'
+local Char = 1 - Symbol
+local Path = Char^1 * Slash
+local NoWord = #(m.P(-1) + Symbol)
+local function whatHappened()
+ return m.Cmt(m.P(1)^1, function (...)
+ print(...)
+ end)
+end
+
+local mt = {}
+mt.__index = mt
+mt.__name = 'matcher'
+
+function mt:exp(state, index)
+ local exp = state[index]
+ if not exp then
+ return
+ end
+ if exp.type == 'word' then
+ return self:word(exp, state, index + 1)
+ elseif exp.type == 'char' then
+ return self:char(exp, state, index + 1)
+ elseif exp.type == '**' then
+ return self:anyPath(exp, state, index + 1)
+ elseif exp.type == '*' then
+ return self:anyChar(exp, state, index + 1)
+ elseif exp.type == '?' then
+ return self:oneChar(exp, state, index + 1)
+ elseif exp.type == '[]' then
+ return self:range(exp, state, index + 1)
+ elseif exp.type == '/' then
+ return self:slash(exp, state, index + 1)
+ end
+end
+
+function mt:word(exp, state, index)
+ local current = self:exp(exp.value, 1)
+ local after = self:exp(state, index)
+ if after then
+ return current * Slash * after
+ else
+ return current
+ end
+end
+
+function mt:char(exp, state, index)
+ local current = m.P(exp.value)
+ local after = self:exp(state, index)
+ if after then
+ return current * after * NoWord
+ else
+ return current * NoWord
+ end
+end
+
+function mt:anyPath(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return m.P {
+ 'Main',
+ Main = after
+ + Path * m.V'Main'
+ }
+ else
+ return Path^0
+ end
+end
+
+function mt:anyChar(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return m.P {
+ 'Main',
+ Main = after
+ + Char * m.V'Main'
+ }
+ else
+ return Char^0
+ end
+end
+
+function mt:oneChar(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return Char * after
+ else
+ return Char
+ end
+end
+
+function mt:range(exp, state, index)
+ local after = self:exp(state, index)
+ local ranges = {}
+ local selects = {}
+ for _, range in ipairs(exp.value) do
+ if #range == 1 then
+ selects[#selects+1] = range[1]
+ elseif #range == 2 then
+ ranges[#ranges+1] = range[1] .. range[2]
+ end
+ end
+ local current = m.S(table.concat(selects)) + m.R(table.unpack(ranges))
+ if after then
+ return current * after
+ else
+ return current
+ end
+end
+
+function mt:slash(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return after
+ else
+ self.needDirectory = true
+ return nil
+ end
+end
+
+function mt:pattern(state)
+ if state.root then
+ return m.C(self:exp(state, 1))
+ else
+ return m.C(self:anyPath(nil, state, 1))
+ end
+end
+
+function mt:isNeedDirectory()
+ return self.needDirectory == true
+end
+
+function mt:isNegative()
+ return self.state.neg == true
+end
+
+function mt:__call(path)
+ return self.matcher:match(path)
+end
+
+return function (state, options)
+ local self = setmetatable({
+ options = options,
+ state = state,
+ }, mt)
+ self.matcher = self:pattern(state)
+ return self
+end
diff --git a/script-beta/src/json/decode.lua b/script-beta/src/json/decode.lua
new file mode 100644
index 00000000..36f8aa54
--- /dev/null
+++ b/script-beta/src/json/decode.lua
@@ -0,0 +1,153 @@
+local lpeg = require 'lpeglabel'
+local tablePack = table.pack
+local rawset = rawset
+local tointeger = math.tointeger
+local tonumber = tonumber
+local setmetatable = setmetatable
+local stringChar = string.char
+local error = error
+
+_ENV = nil
+
+local SaveSort
+local P = lpeg.P
+local S = lpeg.S
+local R = lpeg.R
+local V = lpeg.V
+local C = lpeg.C
+local Ct = lpeg.Ct
+local Cc = lpeg.Cc
+local Cp = lpeg.Cp
+local Cs = lpeg.Cs
+
+local EscMap = {
+ ['t'] = '\t',
+ ['r'] = '\r',
+ ['n'] = '\n',
+ ['"'] = '"',
+ ['\\'] = '\\',
+}
+local BoolMap = {
+ ['true'] = true,
+ ['false'] = false,
+}
+
+local hashmt = {
+ __pairs = function (self)
+ local i = 1
+ local function next()
+ i = i + 1
+ local k = self[i]
+ if k == nil then
+ return
+ end
+ local v = self[k]
+ if v == nil then
+ return next()
+ end
+ return k, v
+ end
+ return next
+ end,
+ __newindex = function (self, k, v)
+ local i = 2
+ while self[i] do
+ i = i + 1
+ end
+ rawset(self, i, k)
+ rawset(self, k, v)
+ end,
+}
+
+-----------------------------------------------------------------------------
+-- JSON4Lua: JSON encoding / decoding support for the Lua language.
+-- json Module.
+-- Author: Craig Mason-Jones
+-- Homepage: http://github.com/craigmj/json4lua/
+-- Version: 1.0.0
+-- This module is released under the MIT License (MIT).
+-- Please see LICENCE.txt for details.
+--
+local function Utf8(str)
+ local n = tonumber(str, 16)
+ -- math.floor(x/2^y) == lazy right shift
+ -- a % 2^b == bitwise_and(a, (2^b)-1)
+ -- 64 = 2^6
+ -- 4096 = 2^12 (or 2^6 * 2^6)
+ local x
+ if n < 0x80 then
+ x = stringChar(n % 0x80)
+ elseif n < 0x800 then
+ -- [110x xxxx] [10xx xxxx]
+ x = stringChar(0xC0 + ((n // 64) % 0x20), 0x80 + (n % 0x40))
+ else
+ -- [1110 xxxx] [10xx xxxx] [10xx xxxx]
+ x = stringChar(0xE0 + ((n // 4096) % 0x10), 0x80 + ((n // 64) % 0x40), 0x80 + (n % 0x40))
+ end
+ return x
+end
+
+local function HashTable(patt)
+ return C(patt) / function (_, ...)
+ local hash = tablePack(...)
+ local n = hash.n
+ hash.n = nil
+ if SaveSort then
+ local max = n // 2
+ for i = 1, max do
+ local key, value = hash[2*i-1], hash[2*i]
+ hash[key] = value
+ hash[i+1] = key
+ end
+ hash[1] = nil
+ for i = max+2, max*2 do
+ hash[i] = nil
+ end
+ return setmetatable(hash, hashmt)
+ else
+ local max = n // 2
+ for i = 1, max do
+ local a = 2*i-1
+ local b = 2*i
+ local key, value = hash[a], hash[b]
+ hash[key] = value
+ hash[a] = nil
+ hash[b] = nil
+ end
+ return hash
+ end
+ end
+end
+
+local Token = P
+{
+ V'Value' * Cp(),
+ Nl = P'\r\n' + S'\r\n',
+ Sp = S' \t' + '//' * (1-V'Nl')^0,
+ Spnl = (V'Sp' + V'Nl')^0,
+ Bool = C(P'true' + P'false') / BoolMap,
+ Int = C('0' + (P'-'^-1 * R'19' * R'09'^0)) / tointeger,
+ Float = C(P'-'^-1 * ('0' + R'19' * R'09'^0) * '.' * R'09'^0) / tonumber,
+ Null = P'null' * Cc(nil),
+ String = '"' * Cs(V'Char'^0) * '"',
+ Char = V'Esc' + V'Utf8' + (1 - P'"' - P'\t' - V'Nl'),
+ Esc = P'\\' * C(S'tnr"\\') / EscMap,
+ Utf8 = P'\\u' * C(P(4)) / Utf8,
+ Hash = V'Spnl' * '{' * V'Spnl' * HashTable((V'Object' + P',' * V'Spnl')^0) * V'Spnl' * P'}' * V'Spnl',
+ Array = V'Spnl' * '[' * V'Spnl' * Ct((V'Value' * V'Spnl' + P',' * V'Spnl')^0) * V'Spnl' * P']' * V'Spnl',
+ Object = V'Spnl' * V'Key' * V'Spnl' * V'Value' * V'Spnl',
+ Key = V'String' * V'Spnl' * ':',
+ Value = V'Hash' + V'Array' + V'Bool' + V'Null' + V'String' + V'Float' + V'Int',
+}
+
+return function (str, save_sort_)
+ SaveSort = save_sort_
+ local table, res, pos = Token:match(str)
+ if not table then
+ if not pos or pos <= #str then
+ pos = pos or 1
+ error(('没匹配完[%s][%s]\n%s'):format(pos, res, str:sub(pos, pos+100)))
+ end
+ end
+ return table
+end
diff --git a/script-beta/src/json/encode.lua b/script-beta/src/json/encode.lua
new file mode 100644
index 00000000..492c5a58
--- /dev/null
+++ b/script-beta/src/json/encode.lua
@@ -0,0 +1,135 @@
+local rep = string.rep
+local gsub = string.gsub
+local sort = table.sort
+local find = string.find
+local tostring = tostring
+local getmetatable = debug.getmetatable
+local type = type
+local next = next
+local pairs = pairs
+local tableConcat = table.concat
+
+_ENV = nil
+
+local index
+local lines
+local n = -1
+local tabs = {}
+
+local esc_map = {
+ ['\\'] = '\\\\',
+ ['\r'] = '\\r',
+ ['\n'] = '\\n',
+ ['\t'] = '\\t',
+ ['"'] = '\\"',
+}
+
+local function encode(data, key)
+ n = n + 1
+ if not tabs[n] then
+ tabs[n] = rep(' ', n)
+ end
+ local tp = type(data)
+ if tp == 'table' then
+ if not data[1] and next(data) then
+ -- 认为这个是哈希表
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": {\r\n'
+ else
+ index=index+1;lines[index] = tabs[n] .. '{\r\n'
+ end
+ local meta = getmetatable(data)
+ local sep
+ if meta and meta.__pairs then
+ for k, v in meta.__pairs(data), data do
+ if encode(v, k) then
+ index=index+1;lines[index] = ',\r\n'
+ sep = true
+ end
+ end
+ else
+ local list = {}
+ local i = 0
+ for k in next, data do
+ i=i+1;list[i] = k
+ end
+ sort(list)
+ for j = 1, i do
+ local k = list[j]
+ if encode(data[k], k) then
+ index=index+1;lines[index] = ',\r\n'
+ sep = true
+ end
+ end
+ end
+ if sep then
+ lines[index] = '\r\n'
+ end
+ index=index+1;lines[index] = tabs[n] .. '}'
+ else
+ -- 认为这个是数组
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": [\r\n'
+ else
+ index=index+1;lines[index] = tabs[n] .. '[\r\n'
+ end
+ local sep
+ for k, v in pairs(data) do
+ if encode(v) then
+ index=index+1;lines[index] = ',\r\n'
+ sep = true
+ end
+ end
+ if sep then
+ lines[index] = '\r\n'
+ end
+ index=index+1;lines[index] = tabs[n] .. ']'
+ end
+ elseif tp == 'number' then
+ data = tostring(data)
+ -- 判断 inf -inf -nan(ind) 1.#INF -1.#INF -1.#IND
+ if find(data, '%a') then
+ data = '0'
+ end
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": ' .. data
+ else
+ index=index+1;lines[index] = tabs[n] .. data
+ end
+ elseif tp == 'boolean' then
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": ' .. tostring(data)
+ else
+ index=index+1;lines[index] = tabs[n] .. tostring(data)
+ end
+ elseif tp == 'nil' then
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": null'
+ else
+ index=index+1;lines[index] = tabs[n] .. 'null'
+ end
+ elseif tp == 'string' then
+ local str = gsub(data, '[\\\r\n\t"]', esc_map)
+ if key then
+ index=index+1;lines[index] = tabs[n] .. '"' .. gsub(key, '[\\\r\n\t"]', esc_map) .. '": "' .. str .. '"'
+ else
+ index=index+1;lines[index] = tabs[n] .. '"' .. str .. '"'
+ end
+ else
+ n = n - 1
+ return false
+ end
+ n = n - 1
+ return true
+end
+
+local function json(t)
+ lines = {}
+ index = 0
+
+ encode(t)
+
+ return tableConcat(lines)
+end
+
+return json
diff --git a/script-beta/src/json/init.lua b/script-beta/src/json/init.lua
new file mode 100644
index 00000000..c28e7aed
--- /dev/null
+++ b/script-beta/src/json/init.lua
@@ -0,0 +1,6 @@
+local api = {
+ decode = require 'json.decode',
+ encode = require 'json.encode',
+}
+
+return api
diff --git a/script-beta/src/jsonrpc.lua b/script-beta/src/jsonrpc.lua
new file mode 100644
index 00000000..5c73f54d
--- /dev/null
+++ b/script-beta/src/jsonrpc.lua
@@ -0,0 +1,41 @@
+local json = require 'json'
+local pcall = pcall
+
+_ENV = nil
+
+---@class jsonrpc
+local m = {}
+m.type = 'jsonrpc'
+
+function m.encode(pack)
+ pack.jsonrpc = '2.0'
+ local content = json.encode(pack)
+ local buf = ('Content-Length: %d\r\n\r\n%s'):format(#content, content)
+ return buf
+end
+
+function m.decode(reader, errHandle)
+ -- 读取协议头
+ local line = reader 'l'
+ -- 不支持修改文本编码
+ if line:find('Content-Type', 1, true) then
+ return nil
+ end
+ local len = line:match('Content%-Length%: (%d+)')
+ if not len then
+ errHandle('Error header: ' .. line)
+ return nil
+ end
+ local content = reader(len + 2)
+ if not content then
+ return nil
+ end
+ local suc, res = pcall(json.decode, content)
+ if not suc then
+ errHandle('Proto parse error: ' .. res)
+ return nil
+ end
+ return res
+end
+
+return m
diff --git a/script-beta/src/language.lua b/script-beta/src/language.lua
new file mode 100644
index 00000000..d1a4b4cf
--- /dev/null
+++ b/script-beta/src/language.lua
@@ -0,0 +1,137 @@
+local fs = require 'bee.filesystem'
+local lni = require 'lni'
+local util = require 'utility'
+
+local function supportLanguage()
+ local list = {}
+ for path in (ROOT / 'locale'):list_directory() do
+ if fs.is_directory(path) then
+ list[#list+1] = path:filename():string():lower()
+ end
+ end
+ return list
+end
+
+local function osLanguage()
+ return LANG:lower()
+end
+
+local function getLanguage(id)
+ local support = supportLanguage()
+ -- 检查是否支持语言
+ if support[id] then
+ return id
+ end
+ -- 根据语言的前2个字母来找近似语言
+ for _, lang in ipairs(support) do
+ if lang:sub(1, 2) == id:sub(1, 2) then
+ return lang
+ end
+ end
+ -- 使用英文
+ return 'enUS'
+end
+
+local function loadFileByLanguage(name, language)
+ local path = ROOT / 'locale' / language / (name .. '.lni')
+ local buf = util.loadFile(path:string())
+ if not buf then
+ return {}
+ end
+ local suc, tbl = xpcall(lni, log.error, buf, path:string())
+ if not suc then
+ return {}
+ end
+ return tbl
+end
+
+local function formatAsArray(str, ...)
+ local index = 0
+ local args = {...}
+ return str:gsub('%{(.-)%}', function (pat)
+ local id, fmt
+ local pos = pat:find(':', 1, true)
+ if pos then
+ id = pat:sub(1, pos-1)
+ fmt = pat:sub(pos+1)
+ else
+ id = pat
+ fmt = 's'
+ end
+ id = tonumber(id)
+ if not id then
+ index = index + 1
+ id = index
+ end
+ return ('%'..fmt):format(args[id])
+ end)
+end
+
+local function formatAsTable(str, ...)
+ local args = ...
+ return str:gsub('%{(.-)%}', function (pat)
+ local id, fmt
+ local pos = pat:find(':', 1, true)
+ if pos then
+ id = pat:sub(1, pos-1)
+ fmt = pat:sub(pos+1)
+ else
+ id = pat
+ fmt = 's'
+ end
+ if not id then
+ return
+ end
+ return ('%'..fmt):format(args[id])
+ end)
+end
+
+local function loadLang(name, language)
+ local tbl = loadFileByLanguage(name, 'en-US')
+ if language ~= 'en-US' then
+ local other = loadFileByLanguage(name, language)
+ for k, v in pairs(other) do
+ tbl[k] = v
+ end
+ end
+ return setmetatable(tbl, {
+ __index = function (self, key)
+ self[key] = key
+ return key
+ end,
+ __call = function (self, key, ...)
+ local str = self[key]
+ local suc, res
+ if type(...) == 'table' then
+ suc, res = pcall(formatAsTable, str, ...)
+ else
+ suc, res = pcall(formatAsArray, str, ...)
+ end
+ if suc then
+ return res
+ else
+ -- 这里不能使用翻译,以免死循环
+ log.warn(('[%s][%s-%s] formated error: %s'):format(
+ language, name, key, str
+ ))
+ return str
+ end
+ end,
+ })
+end
+
+local function init()
+ local id = osLanguage()
+ local language = getLanguage(id)
+ log.info(('VSC language: %s'):format(id))
+ log.info(('LS language: %s'):format(language))
+ return setmetatable({ id = language }, {
+ __index = function (self, name)
+ local tbl = loadLang(name, language)
+ self[name] = tbl
+ return tbl
+ end,
+ })
+end
+
+return init()
diff --git a/script-beta/src/library.lua b/script-beta/src/library.lua
new file mode 100644
index 00000000..d4dba7c9
--- /dev/null
+++ b/script-beta/src/library.lua
@@ -0,0 +1,296 @@
+local lni = require 'lni'
+local fs = require 'bee.filesystem'
+local config = require 'config'
+local util = require 'utility'
+
+local m = {}
+
+local function mergeEnum(lib, locale)
+ if not lib or not locale then
+ return
+ end
+ local pack = {}
+ for _, enum in ipairs(lib) do
+ if enum.enum then
+ pack[enum.enum] = enum
+ end
+ if enum.code then
+ pack[enum.code] = enum
+ end
+ end
+ for _, enum in ipairs(locale) do
+ if pack[enum.enum] then
+ if enum.description then
+ pack[enum.enum].description = enum.description
+ end
+ end
+ if pack[enum.code] then
+ if enum.description then
+ pack[enum.code].description = enum.description
+ end
+ end
+ end
+end
+
+local function mergeField(lib, locale)
+ if not lib or not locale then
+ return
+ end
+ local pack = {}
+ for _, field in ipairs(lib) do
+ if field.field then
+ pack[field.field] = field
+ end
+ end
+ for _, field in ipairs(locale) do
+ if pack[field.field] then
+ if field.description then
+ pack[field.field].description = field.description
+ end
+ end
+ end
+end
+
+local function mergeLocale(libs, locale)
+ if not libs or not locale then
+ return
+ end
+ for name in pairs(locale) do
+ if libs[name] then
+ if locale[name].description then
+ libs[name].description = locale[name].description
+ end
+ mergeEnum(libs[name].enums, locale[name].enums)
+ mergeField(libs[name].fields, locale[name].fields)
+ end
+ end
+end
+
+local function isMatchVersion(version)
+ if not version then
+ return true
+ end
+ local runtimeVersion = config.config.runtime.version
+ if type(version) == 'table' then
+ for i = 1, #version do
+ if version[i] == runtimeVersion then
+ return true
+ end
+ end
+ else
+ if version == runtimeVersion then
+ return true
+ end
+ end
+ return false
+end
+
+local function insertGlobal(tbl, key, value)
+ if not isMatchVersion(value.version) then
+ return false
+ end
+ if not value.doc then
+ value.doc = key
+ end
+ tbl[key] = value
+ return true
+end
+
+local function insertOther(tbl, key, value)
+ if not value.version then
+ return
+ end
+ if not tbl[key] then
+ tbl[key] = {}
+ end
+ if type(value.version) == 'string' then
+ tbl[key][#tbl[key]+1] = value.version
+ elseif type(value.version) == 'table' then
+ for _, version in ipairs(value.version) do
+ if type(version) == 'string' then
+ tbl[key][#tbl[key]+1] = version
+ end
+ end
+ end
+ table.sort(tbl[key])
+end
+
+local function insertCustom(tbl, key, value, libName)
+ if not tbl[key] then
+ tbl[key] = {}
+ end
+ tbl[key][#tbl[key]+1] = libName
+ table.sort(tbl[key])
+end
+
+local function isEnableGlobal(libName)
+ if config.config.runtime.library[libName] then
+ return true
+ end
+ if libName:sub(1, 1) == '@' then
+ return true
+ end
+ return false
+end
+
+local function mergeSource(alllibs, name, lib, libName)
+ if not lib.source then
+ if isEnableGlobal(libName) then
+ local suc = insertGlobal(alllibs.global, name, lib)
+ if not suc then
+ insertOther(alllibs.other, name, lib)
+ end
+ else
+ insertCustom(alllibs.custom, name, lib, libName)
+ end
+ return
+ end
+ for _, source in ipairs(lib.source) do
+ local sourceName = source.name or name
+ if source.type == 'global' then
+ if isEnableGlobal(libName) then
+ local suc = insertGlobal(alllibs.global, sourceName, lib)
+ if not suc then
+ insertOther(alllibs.other, sourceName, lib)
+ end
+ else
+ insertCustom(alllibs.custom, sourceName, lib, libName)
+ end
+ elseif source.type == 'library' then
+ insertGlobal(alllibs.library, sourceName, lib)
+ elseif source.type == 'object' then
+ insertGlobal(alllibs.object, sourceName, lib)
+ end
+ end
+end
+
+local function copy(t)
+ local new = {}
+ for k, v in pairs(t) do
+ new[k] = v
+ end
+ return new
+end
+
+local function insertChild(tbl, name, key, value)
+ if not name or not key then
+ return
+ end
+ if not isMatchVersion(value.version) then
+ return
+ end
+ if not value.doc then
+ value.doc = ('%s.%s'):format(name, key)
+ end
+ if not tbl[name] then
+ tbl[name] = {
+ type = name,
+ name = name,
+ child = {},
+ }
+ end
+ tbl[name].child[key] = copy(value)
+end
+
+local function mergeParent(alllibs, name, lib, libName)
+ for _, parent in ipairs(lib.parent) do
+ if parent.type == 'global' then
+ if isEnableGlobal(libName) then
+ insertChild(alllibs.global, parent.name, name, lib)
+ end
+ elseif parent.type == 'library' then
+ insertChild(alllibs.library, parent.name, name, lib)
+ elseif parent.type == 'object' then
+ insertChild(alllibs.object, parent.name, name, lib)
+ end
+ end
+end
+
+local function mergeLibs(alllibs, libs, libName)
+ if not libs then
+ return
+ end
+ for _, lib in pairs(libs) do
+ if lib.parent then
+ mergeParent(alllibs, lib.name, lib, libName)
+ else
+ mergeSource(alllibs, lib.name, lib, libName)
+ end
+ end
+end
+
+local function loadLocale(language, relative)
+ local localePath = ROOT / 'locale' / language / relative
+ local localeBuf = util.loadFile(localePath:string())
+ if localeBuf then
+ local locale = util.container()
+ xpcall(lni, log.error, localeBuf, localePath:string(), {locale})
+ return locale
+ end
+ return nil
+end
+
+local function fix(libs)
+ for name, lib in pairs(libs) do
+ lib.name = lib.name or name
+ lib.child = {}
+ end
+end
+
+local function scan(path)
+ local result = {path}
+ local i = 0
+ return function ()
+ i = i + 1
+ local current = result[i]
+ if not current then
+ return nil
+ end
+ if fs.is_directory(current) then
+ for path in current:list_directory() do
+ result[#result+1] = path
+ end
+ end
+ return current
+ end
+end
+
+local function init()
+ local lang = require 'language'
+ local id = lang.id
+ m.global = util.container()
+ m.library = util.container()
+ m.object = util.container()
+ m.other = util.container()
+ m.custom = util.container()
+
+ for libPath in (ROOT / 'libs'):list_directory() do
+ local libName = libPath:filename():string()
+ for path in scan(libPath) do
+ local libs
+ local buf = util.loadFile(path:string())
+ if buf then
+ libs = util.container()
+ xpcall(lni, log.error, buf, path:string(), {libs})
+ fix(libs)
+ end
+ local relative = fs.relative(path, ROOT)
+
+ local locale = loadLocale('en-US', relative)
+ mergeLocale(libs, locale)
+ if id ~= 'en-US' then
+ locale = loadLocale(id, relative)
+ mergeLocale(libs, locale)
+ end
+ mergeLibs(m, libs, libName)
+ end
+ end
+end
+
+function m.reload()
+ init()
+end
+
+init()
+
+return m
diff --git a/script-beta/src/log.lua b/script-beta/src/log.lua
new file mode 100644
index 00000000..1a66685a
--- /dev/null
+++ b/script-beta/src/log.lua
@@ -0,0 +1,140 @@
+local fs = require 'bee.filesystem'
+
+local osTime = os.time
+local osClock = os.clock
+local osDate = os.date
+local ioOpen = io.open
+local tablePack = table.pack
+local tableConcat = table.concat
+local tostring = tostring
+local debugTraceBack = debug.traceback
+local mathModf = math.modf
+local debugGetInfo = debug.getinfo
+local ioStdErr = io.stderr
+
+_ENV = nil
+
+local m = {}
+
+m.file = nil
+m.startTime = osTime() - osClock()
+m.size = 0
+m.maxSize = 100 * 1024 * 1024
+
+local function trimSrc(src)
+ src = src:sub(m.prefixLen + 3, -5)
+ src = src:gsub('^[/\\]+', '')
+ src = src:gsub('[\\/]+', '.')
+ return src
+end
+
+local function init_log_file()
+ if not m.file then
+ m.file = ioOpen(m.path, 'w')
+ if not m.file then
+ return
+ end
+ m.file:write('')
+ m.file:close()
+ m.file = ioOpen(m.path, 'ab')
+ if not m.file then
+ return
+ end
+ m.file:setvbuf 'no'
+ end
+end
+
+local function pushLog(level, ...)
+ if not m.path then
+ return
+ end
+ if m.size > m.maxSize then
+ return
+ end
+ local t = tablePack(...)
+ for i = 1, t.n do
+ t[i] = tostring(t[i])
+ end
+ local str = tableConcat(t, '\t', 1, t.n)
+ if level == 'error' then
+ str = str .. '\n' .. debugTraceBack(nil, 3)
+ end
+ local info = debugGetInfo(3, 'Sl')
+ return m.raw(0, level, str, info.source, info.currentline)
+end
+
+function m.info(...)
+ pushLog('info', ...)
+end
+
+function m.debug(...)
+ pushLog('debug', ...)
+end
+
+function m.trace(...)
+ pushLog('trace', ...)
+end
+
+function m.warn(...)
+ pushLog('warn', ...)
+end
+
+function m.error(...)
+ pushLog('error', ...)
+end
+
+function m.raw(thd, level, msg, source, currentline)
+ if level == 'error' then
+ ioStdErr:write(msg .. '\n')
+ end
+ init_log_file()
+ if not m.file then
+ return
+ end
+ local sec, ms = mathModf(m.startTime + osClock())
+ local timestr = osDate('%H:%M:%S', sec)
+ local agl = ''
+ if #level < 5 then
+ agl = (' '):rep(5 - #level)
+ end
+ local buf
+ if currentline == -1 then
+ buf = ('[%s.%03.f][%s]: %s[#%d]%s\n'):format(timestr, ms * 1000, level, agl, thd, msg)
+ else
+ buf = ('[%s.%03.f][%s]: %s[#%d:%s:%s]%s\n'):format(timestr, ms * 1000, level, agl, thd, trimSrc(source), currentline, msg)
+ end
+ m.file:write(buf)
+ m.size = m.size + #buf
+ if m.size > m.maxSize then
+ m.file:write('[REACH MAX SIZE]')
+ end
+ return
+end
+
+function m.init(root, path)
+ local lastBuf
+ if m.file then
+ m.file:close()
+ m.file = nil
+ local file = ioOpen(m.path, 'rb')
+ if file then
+ lastBuf = file:read 'a'
+ file:close()
+ end
+ end
+ m.path = path:string()
+ m.prefixLen = #root:string()
+ m.size = 0
+ if not fs.exists(path:parent_path()) then
+ fs.create_directories(path:parent_path())
+ end
+ if lastBuf then
+ init_log_file()
+ if m.file then
+ m.file:write(lastBuf)
+ m.size = m.size + #lastBuf
+ end
+ end
+end
+
+return m
diff --git a/script-beta/src/parser/ast.lua b/script-beta/src/parser/ast.lua
new file mode 100644
index 00000000..dfd7656d
--- /dev/null
+++ b/script-beta/src/parser/ast.lua
@@ -0,0 +1,1738 @@
+local emmy = require 'parser.emmy'
+
+local tonumber = tonumber
+local stringChar = string.char
+local utf8Char = utf8.char
+local tableUnpack = table.unpack
+local mathType = math.type
+local tableRemove = table.remove
+local pairs = pairs
+local tableSort = table.sort
+
+_ENV = nil
+
+local State
+local PushError
+local PushDiag
+
+-- goto 单独处理
+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,
+}
+
+local VersionOp = {
+ ['&'] = {'Lua 5.3', 'Lua 5.4'},
+ ['~'] = {'Lua 5.3', 'Lua 5.4'},
+ ['|'] = {'Lua 5.3', 'Lua 5.4'},
+ ['<<'] = {'Lua 5.3', 'Lua 5.4'},
+ ['>>'] = {'Lua 5.3', 'Lua 5.4'},
+ ['//'] = {'Lua 5.3', 'Lua 5.4'},
+}
+
+local function checkOpVersion(op)
+ local versions = VersionOp[op.type]
+ if not versions then
+ return
+ end
+ for i = 1, #versions do
+ if versions[i] == State.version then
+ return
+ end
+ end
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = op.start,
+ finish = op.finish,
+ version = versions,
+ info = {
+ version = State.version,
+ }
+ }
+end
+
+local function checkMissEnd(start)
+ if not State.MissEndErr then
+ return
+ end
+ local err = State.MissEndErr
+ State.MissEndErr = nil
+ local _, finish = State.lua:find('[%w_]+', start)
+ if not finish then
+ return
+ end
+ err.info.related = {
+ {
+ start = start,
+ finish = finish,
+ }
+ }
+ PushError {
+ type = 'MISS_END',
+ start = start,
+ finish = finish,
+ }
+end
+
+local function getSelect(vararg, index)
+ return {
+ type = 'select',
+ start = vararg.start,
+ finish = vararg.finish,
+ vararg = vararg,
+ index = index,
+ }
+end
+
+local function getValue(values, i)
+ if not values then
+ return nil, nil
+ end
+ local value = values[i]
+ if not value then
+ local last = values[#values]
+ if not last then
+ return nil, nil
+ end
+ if last.type == 'call' or last.type == 'varargs' then
+ return getSelect(last, i - #values + 1)
+ end
+ return nil, nil
+ end
+ if value.type == 'call' or value.type == 'varargs' then
+ value = getSelect(value, 1)
+ end
+ return value
+end
+
+local function createLocal(key, effect, value, attrs)
+ if not key then
+ return nil
+ end
+ key.type = 'local'
+ key.effect = effect
+ key.value = value
+ key.attrs = attrs
+ if value then
+ key.range = value.finish
+ end
+ return key
+end
+
+local function createCall(args, start, finish)
+ if args then
+ args.type = 'callargs'
+ args.start = start
+ args.finish = finish
+ end
+ return {
+ type = 'call',
+ start = start,
+ finish = finish,
+ args = args,
+ }
+end
+
+local function packList(start, list, finish)
+ local lastFinish = start
+ local wantName = true
+ local count = 0
+ for i = 1, #list do
+ local ast = list[i]
+ if ast.type == ',' then
+ if wantName or i == #list then
+ PushError {
+ type = 'UNEXPECT_SYMBOL',
+ start = ast.start,
+ finish = ast.finish,
+ info = {
+ symbol = ',',
+ }
+ }
+ end
+ wantName = true
+ else
+ if not wantName then
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = lastFinish,
+ finish = ast.start - 1,
+ info = {
+ symbol = ',',
+ }
+ }
+ end
+ wantName = false
+ count = count + 1
+ list[count] = list[i]
+ end
+ lastFinish = ast.finish + 1
+ end
+ for i = count + 1, #list do
+ list[i] = nil
+ end
+ list.type = 'list'
+ list.start = start
+ list.finish = finish - 1
+ return list
+end
+
+local BinaryLevel = {
+ ['or'] = 1,
+ ['and'] = 2,
+ ['<='] = 3,
+ ['>='] = 3,
+ ['<'] = 3,
+ ['>'] = 3,
+ ['~='] = 3,
+ ['=='] = 3,
+ ['|'] = 4,
+ ['~'] = 5,
+ ['&'] = 6,
+ ['<<'] = 7,
+ ['>>'] = 7,
+ ['..'] = 8,
+ ['+'] = 9,
+ ['-'] = 9,
+ ['*'] = 10,
+ ['//'] = 10,
+ ['/'] = 10,
+ ['%'] = 10,
+ ['^'] = 11,
+}
+
+local BinaryForward = {
+ [01] = true,
+ [02] = true,
+ [03] = true,
+ [04] = true,
+ [05] = true,
+ [06] = true,
+ [07] = true,
+ [08] = false,
+ [09] = true,
+ [10] = true,
+ [11] = false,
+}
+
+local Defs = {
+ Nil = function (pos)
+ return {
+ type = 'nil',
+ start = pos,
+ finish = pos + 2,
+ }
+ end,
+ True = function (pos)
+ return {
+ type = 'boolean',
+ start = pos,
+ finish = pos + 3,
+ [1] = true,
+ }
+ end,
+ False = function (pos)
+ return {
+ type = 'boolean',
+ start = pos,
+ finish = pos + 4,
+ [1] = false,
+ }
+ end,
+ LongComment = function (beforeEq, afterEq, str, missPos)
+ if missPos then
+ local endSymbol = ']' .. ('='):rep(afterEq-beforeEq) .. ']'
+ local s, _, w = str:find('(%][%=]*%])[%c%s]*$')
+ if s then
+ PushError {
+ type = 'ERR_LCOMMENT_END',
+ start = missPos - #str + s - 1,
+ finish = missPos - #str + s + #w - 2,
+ info = {
+ symbol = endSymbol,
+ },
+ fix = {
+ title = 'FIX_LCOMMENT_END',
+ {
+ start = missPos - #str + s - 1,
+ finish = missPos - #str + s + #w - 2,
+ text = endSymbol,
+ }
+ },
+ }
+ end
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = missPos,
+ finish = missPos,
+ info = {
+ symbol = endSymbol,
+ },
+ fix = {
+ title = 'ADD_LCOMMENT_END',
+ {
+ start = missPos,
+ finish = missPos,
+ text = endSymbol,
+ }
+ },
+ }
+ end
+ end,
+ CLongComment = function (start1, finish1, start2, finish2)
+ PushError {
+ type = 'ERR_C_LONG_COMMENT',
+ start = start1,
+ finish = finish2 - 1,
+ fix = {
+ title = 'FIX_C_LONG_COMMENT',
+ {
+ start = start1,
+ finish = finish1 - 1,
+ text = '--[[',
+ },
+ {
+ start = start2,
+ finish = finish2 - 1,
+ text = '--]]'
+ },
+ }
+ }
+ end,
+ CCommentPrefix = function (start, finish)
+ PushError {
+ type = 'ERR_COMMENT_PREFIX',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_COMMENT_PREFIX',
+ {
+ start = start,
+ finish = finish - 1,
+ text = '--',
+ },
+ }
+ }
+ end,
+ String = function (start, quote, str, finish)
+ return {
+ type = 'string',
+ start = start,
+ finish = finish - 1,
+ [1] = str,
+ [2] = quote,
+ }
+ end,
+ LongString = function (beforeEq, afterEq, str, missPos)
+ if missPos then
+ local endSymbol = ']' .. ('='):rep(afterEq-beforeEq) .. ']'
+ local s, _, w = str:find('(%][%=]*%])[%c%s]*$')
+ if s then
+ PushError {
+ type = 'ERR_LSTRING_END',
+ start = missPos - #str + s - 1,
+ finish = missPos - #str + s + #w - 2,
+ info = {
+ symbol = endSymbol,
+ },
+ fix = {
+ title = 'FIX_LSTRING_END',
+ {
+ start = missPos - #str + s - 1,
+ finish = missPos - #str + s + #w - 2,
+ text = endSymbol,
+ }
+ },
+ }
+ end
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = missPos,
+ finish = missPos,
+ info = {
+ symbol = endSymbol,
+ },
+ fix = {
+ title = 'ADD_LSTRING_END',
+ {
+ start = missPos,
+ finish = missPos,
+ text = endSymbol,
+ }
+ },
+ }
+ end
+ return '[' .. ('='):rep(afterEq-beforeEq) .. '[', str
+ end,
+ Char10 = function (char)
+ char = tonumber(char)
+ if not char or char < 0 or char > 255 then
+ return ''
+ end
+ return stringChar(char)
+ end,
+ Char16 = function (pos, char)
+ if State.version == 'Lua 5.1' then
+ PushError {
+ type = 'ERR_ESC',
+ start = pos-1,
+ finish = pos,
+ version = {'Lua 5.2', 'Lua 5.3', 'Lua 5.4', 'LuaJIT'},
+ info = {
+ version = State.version,
+ }
+ }
+ return char
+ end
+ return stringChar(tonumber(char, 16))
+ end,
+ CharUtf8 = function (pos, char)
+ if State.version ~= 'Lua 5.3'
+ and State.version ~= 'Lua 5.4'
+ and State.version ~= 'LuaJIT'
+ then
+ PushError {
+ type = 'ERR_ESC',
+ start = pos-3,
+ finish = pos-2,
+ version = {'Lua 5.3', 'Lua 5.4', 'LuaJIT'},
+ info = {
+ version = State.version,
+ }
+ }
+ return char
+ end
+ if #char == 0 then
+ PushError {
+ type = 'UTF8_SMALL',
+ start = pos-3,
+ finish = pos,
+ }
+ return ''
+ end
+ local v = tonumber(char, 16)
+ if not v then
+ for i = 1, #char do
+ if not tonumber(char:sub(i, i), 16) then
+ PushError {
+ type = 'MUST_X16',
+ start = pos + i - 1,
+ finish = pos + i - 1,
+ }
+ end
+ end
+ return ''
+ end
+ if State.version == 'Lua 5.4' then
+ if v < 0 or v > 0x7FFFFFFF then
+ PushError {
+ type = 'UTF8_MAX',
+ start = pos-3,
+ finish = pos+#char,
+ info = {
+ min = '00000000',
+ max = '7FFFFFFF',
+ }
+ }
+ end
+ else
+ if v < 0 or v > 0x10FFFF then
+ PushError {
+ type = 'UTF8_MAX',
+ start = pos-3,
+ finish = pos+#char,
+ version = v <= 0x7FFFFFFF and 'Lua 5.4' or nil,
+ info = {
+ min = '000000',
+ max = '10FFFF',
+ }
+ }
+ end
+ end
+ if v >= 0 and v <= 0x10FFFF then
+ return utf8Char(v)
+ end
+ return ''
+ end,
+ Number = function (start, number, finish)
+ local n = tonumber(number)
+ if n then
+ State.LastNumber = {
+ type = 'number',
+ start = start,
+ finish = finish - 1,
+ [1] = n,
+ }
+ return State.LastNumber
+ else
+ PushError {
+ type = 'MALFORMED_NUMBER',
+ start = start,
+ finish = finish - 1,
+ }
+ State.LastNumber = {
+ type = 'number',
+ start = start,
+ finish = finish - 1,
+ [1] = 0,
+ }
+ return State.LastNumber
+ end
+ end,
+ FFINumber = function (start, symbol)
+ local lastNumber = State.LastNumber
+ if mathType(lastNumber[1]) == 'float' then
+ PushError {
+ type = 'UNKNOWN_SYMBOL',
+ start = start,
+ finish = start + #symbol - 1,
+ info = {
+ symbol = symbol,
+ }
+ }
+ lastNumber[1] = 0
+ return
+ end
+ if State.version ~= 'LuaJIT' then
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = start,
+ finish = start + #symbol - 1,
+ version = 'LuaJIT',
+ info = {
+ version = State.version,
+ }
+ }
+ lastNumber[1] = 0
+ end
+ end,
+ ImaginaryNumber = function (start, symbol)
+ local lastNumber = State.LastNumber
+ if State.version ~= 'LuaJIT' then
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = start,
+ finish = start + #symbol - 1,
+ version = 'LuaJIT',
+ info = {
+ version = State.version,
+ }
+ }
+ end
+ lastNumber[1] = 0
+ end,
+ Name = function (start, str, finish)
+ local isKeyWord
+ if RESERVED[str] then
+ isKeyWord = true
+ elseif str == 'goto' then
+ if State.version ~= 'Lua 5.1' and State.version ~= 'LuaJIT' then
+ isKeyWord = true
+ end
+ end
+ if isKeyWord then
+ PushError {
+ type = 'KEYWORD',
+ start = start,
+ finish = finish - 1,
+ }
+ end
+ return {
+ type = 'name',
+ start = start,
+ finish = finish - 1,
+ [1] = str,
+ }
+ end,
+ GetField = function (dot, field)
+ local obj = {
+ type = 'getfield',
+ field = field,
+ dot = dot,
+ start = dot.start,
+ finish = (field or dot).finish,
+ }
+ if field then
+ field.type = 'field'
+ field.parent = obj
+ end
+ return obj
+ end,
+ GetIndex = function (start, index, finish)
+ local obj = {
+ type = 'getindex',
+ start = start,
+ finish = finish - 1,
+ index = index,
+ }
+ if index then
+ index.parent = obj
+ end
+ return obj
+ end,
+ GetMethod = function (colon, method)
+ local obj = {
+ type = 'getmethod',
+ method = method,
+ colon = colon,
+ start = colon.start,
+ finish = (method or colon).finish,
+ }
+ if method then
+ method.type = 'method'
+ method.parent = obj
+ end
+ return obj
+ end,
+ Single = function (unit)
+ unit.type = 'getname'
+ return unit
+ end,
+ Simple = function (units)
+ local last = units[1]
+ for i = 2, #units do
+ local current = units[i]
+ current.node = last
+ current.start = last.start
+ last.next = current
+ last = units[i]
+ end
+ return last
+ end,
+ SimpleCall = function (call)
+ if call.type ~= 'call' and call.type ~= 'getmethod' then
+ PushError {
+ type = 'EXP_IN_ACTION',
+ start = call.start,
+ finish = call.finish,
+ }
+ end
+ return call
+ end,
+ BinaryOp = function (start, op)
+ return {
+ type = op,
+ start = start,
+ finish = start + #op - 1,
+ }
+ end,
+ UnaryOp = function (start, op)
+ return {
+ type = op,
+ start = start,
+ finish = start + #op - 1,
+ }
+ end,
+ Unary = function (first, ...)
+ if not ... then
+ return nil
+ end
+ local list = {first, ...}
+ local e = list[#list]
+ for i = #list - 1, 1, -1 do
+ local op = list[i]
+ checkOpVersion(op)
+ e = {
+ type = 'unary',
+ op = op,
+ start = op.start,
+ finish = e.finish,
+ [1] = e,
+ }
+ end
+ return e
+ end,
+ SubBinary = function (op, symb)
+ if symb then
+ return op, symb
+ end
+ PushError {
+ type = 'MISS_EXP',
+ start = op.start,
+ finish = op.finish,
+ }
+ end,
+ Binary = function (first, op, second, ...)
+ if not first then
+ return second
+ end
+ if not op then
+ return first
+ end
+ if not ... then
+ checkOpVersion(op)
+ return {
+ type = 'binary',
+ op = op,
+ start = first.start,
+ finish = second.finish,
+ [1] = first,
+ [2] = second,
+ }
+ end
+ local list = {first, op, second, ...}
+ local ops = {}
+ for i = 2, #list, 2 do
+ ops[#ops+1] = i
+ end
+ tableSort(ops, function (a, b)
+ local op1 = list[a]
+ local op2 = list[b]
+ local lv1 = BinaryLevel[op1.type]
+ local lv2 = BinaryLevel[op2.type]
+ if lv1 == lv2 then
+ local forward = BinaryForward[lv1]
+ if forward then
+ return op1.start > op2.start
+ else
+ return op1.start < op2.start
+ end
+ else
+ return lv1 < lv2
+ end
+ end)
+ local final
+ for i = #ops, 1, -1 do
+ local n = ops[i]
+ local op = list[n]
+ local left = list[n-1]
+ local right = list[n+1]
+ local exp = {
+ type = 'binary',
+ op = op,
+ start = left.start,
+ finish = right and right.finish or op.finish,
+ [1] = left,
+ [2] = right,
+ }
+ local leftIndex, rightIndex
+ if list[left] then
+ leftIndex = list[left[1]]
+ else
+ leftIndex = n - 1
+ end
+ if list[right] then
+ rightIndex = list[right[2]]
+ else
+ rightIndex = n + 1
+ end
+
+ list[leftIndex] = exp
+ list[rightIndex] = exp
+ list[left] = leftIndex
+ list[right] = rightIndex
+ list[exp] = n
+ final = exp
+
+ checkOpVersion(op)
+ end
+ return final
+ end,
+ Paren = function (start, exp, finish)
+ if exp and exp.type == 'paren' then
+ exp.start = start
+ exp.finish = finish - 1
+ return exp
+ end
+ return {
+ type = 'paren',
+ start = start,
+ finish = finish - 1,
+ exp = exp
+ }
+ end,
+ VarArgs = function (dots)
+ dots.type = 'varargs'
+ return dots
+ end,
+ PackLoopArgs = function (start, list, finish)
+ local list = packList(start, list, finish)
+ if #list == 0 then
+ PushError {
+ type = 'MISS_LOOP_MIN',
+ start = finish,
+ finish = finish,
+ }
+ elseif #list == 1 then
+ PushError {
+ type = 'MISS_LOOP_MAX',
+ start = finish,
+ finish = finish,
+ }
+ end
+ return list
+ end,
+ PackInNameList = function (start, list, finish)
+ local list = packList(start, list, finish)
+ if #list == 0 then
+ PushError {
+ type = 'MISS_NAME',
+ start = start,
+ finish = finish,
+ }
+ end
+ return list
+ end,
+ PackInExpList = function (start, list, finish)
+ local list = packList(start, list, finish)
+ if #list == 0 then
+ PushError {
+ type = 'MISS_EXP',
+ start = start,
+ finish = finish,
+ }
+ end
+ return list
+ end,
+ PackExpList = function (start, list, finish)
+ local list = packList(start, list, finish)
+ return list
+ end,
+ PackNameList = function (start, list, finish)
+ local list = packList(start, list, finish)
+ return list
+ end,
+ Call = function (start, args, finish)
+ return createCall(args, start, finish-1)
+ end,
+ COMMA = function (start)
+ return {
+ type = ',',
+ start = start,
+ finish = start,
+ }
+ end,
+ SEMICOLON = function (start)
+ return {
+ type = ';',
+ start = start,
+ finish = start,
+ }
+ end,
+ DOTS = function (start)
+ return {
+ type = '...',
+ start = start,
+ finish = start + 2,
+ }
+ end,
+ COLON = function (start)
+ return {
+ type = ':',
+ start = start,
+ finish = start,
+ }
+ end,
+ DOT = function (start)
+ return {
+ type = '.',
+ start = start,
+ finish = start,
+ }
+ end,
+ Function = function (functionStart, functionFinish, args, actions, endStart, endFinish)
+ actions.type = 'function'
+ actions.start = functionStart
+ actions.finish = endFinish - 1
+ actions.args = args
+ actions.keyword= {
+ functionStart, functionFinish - 1,
+ endStart, endFinish - 1,
+ }
+ checkMissEnd(functionStart)
+ return actions
+ end,
+ NamedFunction = function (functionStart, functionFinish, name, args, actions, endStart, endFinish)
+ actions.type = 'function'
+ actions.start = functionStart
+ actions.finish = endFinish - 1
+ actions.args = args
+ actions.keyword= {
+ functionStart, functionFinish - 1,
+ endStart, endFinish - 1,
+ }
+ checkMissEnd(functionStart)
+ if not name then
+ return
+ end
+ if name.type == 'getname' then
+ name.type = 'setname'
+ name.value = actions
+ elseif name.type == 'getfield' then
+ name.type = 'setfield'
+ name.value = actions
+ elseif name.type == 'getmethod' then
+ name.type = 'setmethod'
+ name.value = actions
+ end
+ name.range = actions.finish
+ name.vstart = functionStart
+ return name
+ end,
+ LocalFunction = function (start, functionStart, functionFinish, name, args, actions, endStart, endFinish)
+ actions.type = 'function'
+ actions.start = start
+ actions.finish = endFinish - 1
+ actions.args = args
+ actions.keyword= {
+ functionStart, functionFinish - 1,
+ endStart, endFinish - 1,
+ }
+ checkMissEnd(start)
+
+ if not name then
+ return
+ end
+
+ if name.type ~= 'getname' then
+ PushError {
+ type = 'UNEXPECT_LFUNC_NAME',
+ start = name.start,
+ finish = name.finish,
+ }
+ return
+ end
+
+ local loc = createLocal(name, name.start, actions)
+ loc.localfunction = true
+ loc.vstart = functionStart
+
+ return loc
+ end,
+ Table = function (start, tbl, finish)
+ tbl.type = 'table'
+ tbl.start = start
+ tbl.finish = finish - 1
+ local wantField = true
+ local lastStart = start + 1
+ local fieldCount = 0
+ for i = 1, #tbl do
+ local field = tbl[i]
+ if field.type == ',' or field.type == ';' then
+ if wantField then
+ PushError {
+ type = 'MISS_EXP',
+ start = lastStart,
+ finish = field.start - 1,
+ }
+ end
+ wantField = true
+ lastStart = field.finish + 1
+ else
+ if not wantField then
+ PushError {
+ type = 'MISS_SEP_IN_TABLE',
+ start = lastStart,
+ finish = field.start - 1,
+ }
+ end
+ wantField = false
+ lastStart = field.finish + 1
+ fieldCount = fieldCount + 1
+ tbl[fieldCount] = field
+ end
+ end
+ for i = fieldCount + 1, #tbl do
+ tbl[i] = nil
+ end
+ return tbl
+ end,
+ NewField = function (start, field, value, finish)
+ local obj = {
+ type = 'tablefield',
+ start = start,
+ finish = finish-1,
+ field = field,
+ value = value,
+ }
+ if field then
+ field.type = 'field'
+ field.parent = obj
+ end
+ return obj
+ end,
+ NewIndex = function (start, index, value, finish)
+ local obj = {
+ type = 'tableindex',
+ start = start,
+ finish = finish-1,
+ index = index,
+ value = value,
+ }
+ if index then
+ index.parent = obj
+ end
+ return obj
+ end,
+ FuncArgs = function (start, args, finish)
+ args.type = 'funcargs'
+ args.start = start
+ args.finish = finish - 1
+ local lastStart = start + 1
+ local wantName = true
+ local argCount = 0
+ for i = 1, #args do
+ local arg = args[i]
+ local argAst = arg
+ if argAst.type == ',' then
+ if wantName then
+ PushError {
+ type = 'MISS_NAME',
+ start = lastStart,
+ finish = argAst.start-1,
+ }
+ end
+ wantName = true
+ else
+ if not wantName then
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = lastStart-1,
+ finish = argAst.start-1,
+ info = {
+ symbol = ',',
+ }
+ }
+ end
+ wantName = false
+ argCount = argCount + 1
+
+ if argAst.type == '...' then
+ args[argCount] = arg
+ if i < #args then
+ local a = args[i+1]
+ local b = args[#args]
+ PushError {
+ type = 'ARGS_AFTER_DOTS',
+ start = a.start,
+ finish = b.finish,
+ }
+ end
+ break
+ else
+ args[argCount] = createLocal(arg, arg.start)
+ end
+ end
+ lastStart = argAst.finish + 1
+ end
+ for i = argCount + 1, #args do
+ args[i] = nil
+ end
+ if wantName and argCount > 0 then
+ PushError {
+ type = 'MISS_NAME',
+ start = lastStart,
+ finish = finish - 1,
+ }
+ end
+ return args
+ end,
+ Set = function (start, keys, values, finish)
+ for i = 1, #keys do
+ local key = keys[i]
+ if key.type == 'getname' then
+ key.type = 'setname'
+ key.value = getValue(values, i)
+ elseif key.type == 'getfield' then
+ key.type = 'setfield'
+ key.value = getValue(values, i)
+ elseif key.type == 'getindex' then
+ key.type = 'setindex'
+ key.value = getValue(values, i)
+ end
+ if key.value then
+ key.range = key.value.finish
+ end
+ end
+ if values then
+ for i = #keys+1, #values do
+ local value = values[i]
+ PushDiag('redundant-value', {
+ start = value.start,
+ finish = value.finish,
+ max = #keys,
+ passed = #values,
+ })
+ end
+ end
+ return tableUnpack(keys)
+ end,
+ LocalAttr = function (attrs)
+ for i = 1, #attrs do
+ local attr = attrs[i]
+ local attrAst = attr
+ attrAst.type = 'localattr'
+ if State.version ~= 'Lua 5.4' then
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = attrAst.start,
+ finish = attrAst.finish,
+ version = 'Lua 5.4',
+ info = {
+ version = State.version,
+ }
+ }
+ elseif attrAst[1] ~= 'const' and attrAst[1] ~= 'close' then
+ PushError {
+ type = 'UNKNOWN_TAG',
+ start = attrAst.start,
+ finish = attrAst.finish,
+ info = {
+ tag = attrAst[1],
+ }
+ }
+ elseif i > 1 then
+ PushError {
+ type = 'MULTI_TAG',
+ start = attrAst.start,
+ finish = attrAst.finish,
+ info = {
+ tag = attrAst[1],
+ }
+ }
+ end
+ end
+ return attrs
+ end,
+ LocalName = function (name, attrs)
+ if not name then
+ return name
+ end
+ name.attrs = attrs
+ return name
+ end,
+ Local = function (start, keys, values, finish)
+ for i = 1, #keys do
+ local key = keys[i]
+ local attrs = key.attrs
+ key.attrs = nil
+ local value = getValue(values, i)
+ createLocal(key, finish, value, attrs)
+ end
+ if values then
+ for i = #keys+1, #values do
+ local value = values[i]
+ PushDiag('redundant-value', {
+ start = value.start,
+ finish = value.finish,
+ max = #keys,
+ passed = #values,
+ })
+ end
+ end
+ return tableUnpack(keys)
+ end,
+ Do = function (start, actions, endA, endB)
+ actions.type = 'do'
+ actions.start = start
+ actions.finish = endB - 1
+ actions.keyword= {
+ start, start + #'do' - 1,
+ endA , endB - 1,
+ }
+ checkMissEnd(start)
+ return actions
+ end,
+ Break = function (start, finish)
+ return {
+ type = 'break',
+ start = start,
+ finish = finish - 1,
+ }
+ end,
+ Return = function (start, exps, finish)
+ exps.type = 'return'
+ exps.start = start
+ exps.finish = finish - 1
+ return exps
+ end,
+ Label = function (start, name, finish)
+ if State.version == 'Lua 5.1' then
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = start,
+ finish = finish - 1,
+ version = {'Lua 5.2', 'Lua 5.3', 'Lua 5.4', 'LuaJIT'},
+ info = {
+ version = State.version,
+ }
+ }
+ return
+ end
+ if not name then
+ return nil
+ end
+ name.type = 'label'
+ return name
+ end,
+ GoTo = function (start, name, finish)
+ if State.version == 'Lua 5.1' then
+ PushError {
+ type = 'UNSUPPORT_SYMBOL',
+ start = start,
+ finish = finish - 1,
+ version = {'Lua 5.2', 'Lua 5.3', 'Lua 5.4', 'LuaJIT'},
+ info = {
+ version = State.version,
+ }
+ }
+ return
+ end
+ if not name then
+ return nil
+ end
+ name.type = 'goto'
+ return name
+ end,
+ IfBlock = function (ifStart, ifFinish, exp, thenStart, thenFinish, actions, finish)
+ actions.type = 'ifblock'
+ actions.start = ifStart
+ actions.finish = finish - 1
+ actions.filter = exp
+ actions.keyword= {
+ ifStart, ifFinish - 1,
+ thenStart, thenFinish - 1,
+ }
+ return actions
+ end,
+ ElseIfBlock = function (elseifStart, elseifFinish, exp, thenStart, thenFinish, actions, finish)
+ actions.type = 'elseifblock'
+ actions.start = elseifStart
+ actions.finish = finish - 1
+ actions.filter = exp
+ actions.keyword= {
+ elseifStart, elseifFinish - 1,
+ thenStart, thenFinish - 1,
+ }
+ return actions
+ end,
+ ElseBlock = function (elseStart, elseFinish, actions, finish)
+ actions.type = 'elseblock'
+ actions.start = elseStart
+ actions.finish = finish - 1
+ actions.keyword= {
+ elseStart, elseFinish - 1,
+ }
+ return actions
+ end,
+ If = function (start, blocks, endStart, endFinish)
+ blocks.type = 'if'
+ blocks.start = start
+ blocks.finish = endFinish - 1
+ local hasElse
+ for i = 1, #blocks do
+ local block = blocks[i]
+ if i == 1 and block.type ~= 'ifblock' then
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = block.start,
+ finish = block.start,
+ info = {
+ symbol = 'if',
+ }
+ }
+ end
+ if hasElse then
+ PushError {
+ type = 'BLOCK_AFTER_ELSE',
+ start = block.start,
+ finish = block.finish,
+ }
+ end
+ if block.type == 'elseblock' then
+ hasElse = true
+ end
+ end
+ checkMissEnd(start)
+ return blocks
+ end,
+ Loop = function (forA, forB, arg, steps, doA, doB, blockStart, block, endA, endB)
+ local loc = createLocal(arg, blockStart, steps[1])
+ block.type = 'loop'
+ block.start = forA
+ block.finish = endB - 1
+ block.loc = loc
+ block.max = steps[2]
+ block.step = steps[3]
+ block.keyword= {
+ forA, forB - 1,
+ doA , doB - 1,
+ endA, endB - 1,
+ }
+ checkMissEnd(forA)
+ return block
+ end,
+ In = function (forA, forB, keys, inA, inB, exp, doA, doB, blockStart, block, endA, endB)
+ local func = tableRemove(exp, 1)
+ block.type = 'in'
+ block.start = forA
+ block.finish = endB - 1
+ block.keys = keys
+ block.keyword= {
+ forA, forB - 1,
+ inA , inB - 1,
+ doA , doB - 1,
+ endA, endB - 1,
+ }
+
+ local values
+ if func then
+ local call = createCall(exp, func.finish + 1, exp.finish)
+ call.node = func
+ call.start = func.start
+ func.next = call
+ values = { call }
+ keys.range = call.finish
+ end
+ for i = 1, #keys do
+ local loc = keys[i]
+ if values then
+ createLocal(loc, blockStart, getValue(values, i))
+ else
+ createLocal(loc, blockStart)
+ end
+ end
+ checkMissEnd(forA)
+ return block
+ end,
+ While = function (whileA, whileB, filter, doA, doB, block, endA, endB)
+ block.type = 'while'
+ block.start = whileA
+ block.finish = endB - 1
+ block.filter = filter
+ block.keyword= {
+ whileA, whileB - 1,
+ doA , doB - 1,
+ endA , endB - 1,
+ }
+ checkMissEnd(whileA)
+ return block
+ end,
+ Repeat = function (repeatA, repeatB, block, untilA, untilB, filter, finish)
+ block.type = 'repeat'
+ block.start = repeatA
+ block.finish = finish
+ block.filter = filter
+ block.keyword= {
+ repeatA, repeatB - 1,
+ untilA , untilB - 1,
+ }
+ return block
+ end,
+ Lua = function (start, actions, finish)
+ actions.type = 'main'
+ actions.start = start
+ actions.finish = finish - 1
+ return actions
+ end,
+
+ -- 捕获错误
+ UnknownSymbol = function (start, symbol)
+ PushError {
+ type = 'UNKNOWN_SYMBOL',
+ start = start,
+ finish = start + #symbol - 1,
+ info = {
+ symbol = symbol,
+ }
+ }
+ return
+ end,
+ UnknownAction = function (start, symbol)
+ PushError {
+ type = 'UNKNOWN_SYMBOL',
+ start = start,
+ finish = start + #symbol - 1,
+ info = {
+ symbol = symbol,
+ }
+ }
+ end,
+ DirtyName = function (pos)
+ PushError {
+ type = 'MISS_NAME',
+ start = pos,
+ finish = pos,
+ }
+ return nil
+ end,
+ DirtyExp = function (pos)
+ PushError {
+ type = 'MISS_EXP',
+ start = pos,
+ finish = pos,
+ }
+ return nil
+ end,
+ MissExp = function (pos)
+ PushError {
+ type = 'MISS_EXP',
+ start = pos,
+ finish = pos,
+ }
+ end,
+ MissExponent = function (start, finish)
+ PushError {
+ type = 'MISS_EXPONENT',
+ start = start,
+ finish = finish - 1,
+ }
+ end,
+ MissQuote1 = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '"'
+ }
+ }
+ end,
+ MissQuote2 = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = "'"
+ }
+ }
+ end,
+ MissEscX = function (pos)
+ PushError {
+ type = 'MISS_ESC_X',
+ start = pos-2,
+ finish = pos+1,
+ }
+ end,
+ MissTL = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '{',
+ }
+ }
+ end,
+ MissTR = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '}',
+ }
+ }
+ end,
+ MissBR = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = ']',
+ }
+ }
+ end,
+ MissPL = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '(',
+ }
+ }
+ end,
+ MissPR = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = ')',
+ }
+ }
+ end,
+ ErrEsc = function (pos)
+ PushError {
+ type = 'ERR_ESC',
+ start = pos-1,
+ finish = pos,
+ }
+ end,
+ MustX16 = function (pos, str)
+ PushError {
+ type = 'MUST_X16',
+ start = pos,
+ finish = pos + #str - 1,
+ }
+ end,
+ MissAssign = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '=',
+ }
+ }
+ end,
+ MissTableSep = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = ','
+ }
+ }
+ end,
+ MissField = function (pos)
+ PushError {
+ type = 'MISS_FIELD',
+ start = pos,
+ finish = pos,
+ }
+ end,
+ MissMethod = function (pos)
+ PushError {
+ type = 'MISS_METHOD',
+ start = pos,
+ finish = pos,
+ }
+ end,
+ MissLabel = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = '::',
+ }
+ }
+ end,
+ MissEnd = function (pos)
+ State.MissEndErr = PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = 'end',
+ }
+ }
+ return pos, pos
+ end,
+ MissDo = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = 'do',
+ }
+ }
+ return pos, pos
+ end,
+ MissComma = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = ',',
+ }
+ }
+ end,
+ MissIn = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = 'in',
+ }
+ }
+ return pos, pos
+ end,
+ MissUntil = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = 'until',
+ }
+ }
+ return pos, pos
+ end,
+ MissThen = function (pos)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = pos,
+ finish = pos,
+ info = {
+ symbol = 'then',
+ }
+ }
+ return pos, pos
+ end,
+ MissName = function (pos)
+ PushError {
+ type = 'MISS_NAME',
+ start = pos,
+ finish = pos,
+ }
+ end,
+ ExpInAction = function (start, exp, finish)
+ PushError {
+ type = 'EXP_IN_ACTION',
+ start = start,
+ finish = finish - 1,
+ }
+ return exp
+ end,
+ MissIf = function (start, block)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = start,
+ finish = start,
+ info = {
+ symbol = 'if',
+ }
+ }
+ return block
+ end,
+ MissGT = function (start)
+ PushError {
+ type = 'MISS_SYMBOL',
+ start = start,
+ finish = start,
+ info = {
+ symbol = '>'
+ }
+ }
+ end,
+ ErrAssign = function (start, finish)
+ PushError {
+ type = 'ERR_ASSIGN_AS_EQ',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_ASSIGN_AS_EQ',
+ {
+ start = start,
+ finish = finish - 1,
+ text = '=',
+ }
+ }
+ }
+ end,
+ ErrEQ = function (start, finish)
+ PushError {
+ type = 'ERR_EQ_AS_ASSIGN',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_EQ_AS_ASSIGN',
+ {
+ start = start,
+ finish = finish - 1,
+ text = '==',
+ }
+ }
+ }
+ return '=='
+ end,
+ ErrUEQ = function (start, finish)
+ PushError {
+ type = 'ERR_UEQ',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_UEQ',
+ {
+ start = start,
+ finish = finish - 1,
+ text = '~=',
+ }
+ }
+ }
+ return '=='
+ end,
+ ErrThen = function (start, finish)
+ PushError {
+ type = 'ERR_THEN_AS_DO',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_THEN_AS_DO',
+ {
+ start = start,
+ finish = finish - 1,
+ text = 'then',
+ }
+ }
+ }
+ return start, finish
+ end,
+ ErrDo = function (start, finish)
+ PushError {
+ type = 'ERR_DO_AS_THEN',
+ start = start,
+ finish = finish - 1,
+ fix = {
+ title = 'FIX_DO_AS_THEN',
+ {
+ start = start,
+ finish = finish - 1,
+ text = 'do',
+ }
+ }
+ }
+ return start, finish
+ end,
+}
+
+--for k, v in pairs(emmy.ast) do
+-- Defs[k] = v
+--end
+
+local function init(state)
+ State = state
+ PushError = state.pushError
+ PushDiag = state.pushDiag
+ emmy.init(State)
+end
+
+local function close()
+ State = nil
+ PushError = nil
+ PushDiag = nil
+end
+
+return {
+ defs = Defs,
+ init = init,
+ close = close,
+}
diff --git a/script-beta/src/parser/calcline.lua b/script-beta/src/parser/calcline.lua
new file mode 100644
index 00000000..26f475d9
--- /dev/null
+++ b/script-beta/src/parser/calcline.lua
@@ -0,0 +1,93 @@
+local m = require 'lpeglabel'
+
+local row
+local fl
+local NL = (m.P'\r\n' + m.S'\r\n') * m.Cp() / function (pos)
+ row = row + 1
+ fl = pos
+end
+local ROWCOL = (NL + m.P(1))^0
+local function rowcol(str, n)
+ row = 1
+ fl = 1
+ ROWCOL:match(str:sub(1, n))
+ local col = n - fl + 1
+ return row, col
+end
+
+local function rowcol_utf8(str, n)
+ row = 1
+ fl = 1
+ ROWCOL:match(str:sub(1, n))
+ return row, utf8.len(str, fl, n)
+end
+
+local function position(str, _row, _col)
+ local cur = 1
+ local row = 1
+ while true do
+ if row == _row then
+ return cur + _col - 1
+ elseif row > _row then
+ return cur - 1
+ end
+ local pos = str:find('[\r\n]', cur)
+ if not pos then
+ return #str
+ end
+ row = row + 1
+ if str:sub(pos, pos+1) == '\r\n' then
+ cur = pos + 2
+ else
+ cur = pos + 1
+ end
+ end
+end
+
+local function position_utf8(str, _row, _col)
+ local cur = 1
+ local row = 1
+ while true do
+ if row == _row then
+ return utf8.offset(str, _col, cur)
+ elseif row > _row then
+ return cur - 1
+ end
+ local pos = str:find('[\r\n]', cur)
+ if not pos then
+ return #str
+ end
+ row = row + 1
+ if str:sub(pos, pos+1) == '\r\n' then
+ cur = pos + 2
+ else
+ cur = pos + 1
+ end
+ end
+end
+
+local NL = m.P'\r\n' + m.S'\r\n'
+
+local function line(str, row)
+ local count = 0
+ local res
+ local LINE = m.Cmt((1 - NL)^0, function (_, _, c)
+ count = count + 1
+ if count == row then
+ res = c
+ return false
+ end
+ return true
+ end)
+ local MATCH = (LINE * NL)^0 * LINE
+ MATCH:match(str)
+ return res
+end
+
+return {
+ rowcol = rowcol,
+ rowcol_utf8 = rowcol_utf8,
+ position = position,
+ position_utf8 = position_utf8,
+ line = line,
+}
diff --git a/script-beta/src/parser/compile.lua b/script-beta/src/parser/compile.lua
new file mode 100644
index 00000000..bcd9ecc8
--- /dev/null
+++ b/script-beta/src/parser/compile.lua
@@ -0,0 +1,549 @@
+local guide = require 'parser.guide'
+local type = type
+
+local specials = {
+ ['_G'] = true,
+ ['rawset'] = true,
+ ['rawget'] = true,
+ ['setmetatable'] = true,
+ ['require'] = true,
+ ['dofile'] = true,
+ ['loadfile'] = true,
+ ['pcall'] = true,
+ ['xpcall'] = true,
+}
+
+_ENV = nil
+
+local LocalLimit = 200
+local pushError, Compile, CompileBlock, Block, GoToTag, ENVMode, Compiled, LocalCount, Version, Root
+
+local function addRef(node, obj)
+ if not node.ref then
+ node.ref = {}
+ end
+ node.ref[#node.ref+1] = obj
+ obj.node = node
+end
+
+local function addSpecial(name, obj)
+ if not Root.specials then
+ Root.specials = {}
+ end
+ if not Root.specials[name] then
+ Root.specials[name] = {}
+ end
+ Root.specials[name][#Root.specials[name]+1] = obj
+ obj.special = name
+end
+
+local vmMap = {
+ ['getname'] = function (obj)
+ local loc = guide.getLocal(obj, obj[1], obj.start)
+ if loc then
+ obj.type = 'getlocal'
+ obj.loc = loc
+ addRef(loc, obj)
+ if loc.special then
+ addSpecial(loc.special, obj)
+ end
+ else
+ obj.type = 'getglobal'
+ if ENVMode == '_ENV' then
+ local node = guide.getLocal(obj, '_ENV', obj.start)
+ if node then
+ addRef(node, obj)
+ end
+ end
+ local name = obj[1]
+ if specials[name] then
+ addSpecial(name, obj)
+ end
+ end
+ return obj
+ end,
+ ['getfield'] = function (obj)
+ Compile(obj.node, obj)
+ end,
+ ['call'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.args, obj)
+ end,
+ ['callargs'] = function (obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ end,
+ ['binary'] = function (obj)
+ Compile(obj[1], obj)
+ Compile(obj[2], obj)
+ end,
+ ['unary'] = function (obj)
+ Compile(obj[1], obj)
+ end,
+ ['varargs'] = function (obj)
+ local func = guide.getParentFunction(obj)
+ if func then
+ local index, vararg = guide.getFunctionVarArgs(func)
+ if not index then
+ pushError {
+ type = 'UNEXPECT_DOTS',
+ start = obj.start,
+ finish = obj.finish,
+ }
+ end
+ if vararg then
+ if not vararg.ref then
+ vararg.ref = {}
+ end
+ vararg.ref[#vararg.ref+1] = obj
+ end
+ end
+ end,
+ ['paren'] = function (obj)
+ Compile(obj.exp, obj)
+ end,
+ ['getindex'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.index, obj)
+ end,
+ ['setindex'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.index, obj)
+ Compile(obj.value, obj)
+ end,
+ ['getmethod'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.method, obj)
+ end,
+ ['setmethod'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.method, obj)
+ local value = obj.value
+ value.localself = {
+ type = 'local',
+ start = 0,
+ finish = 0,
+ method = obj,
+ effect = obj.finish,
+ tag = 'self',
+ [1] = 'self',
+ }
+ Compile(value, obj)
+ end,
+ ['function'] = function (obj)
+ local lastBlock = Block
+ local LastLocalCount = LocalCount
+ Block = obj
+ LocalCount = 0
+ if obj.localself then
+ Compile(obj.localself, obj)
+ obj.localself = nil
+ end
+ Compile(obj.args, obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ Block = lastBlock
+ LocalCount = LastLocalCount
+ end,
+ ['funcargs'] = function (obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ end,
+ ['table'] = function (obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ end,
+ ['tablefield'] = function (obj)
+ Compile(obj.value, obj)
+ end,
+ ['tableindex'] = function (obj)
+ Compile(obj.index, obj)
+ Compile(obj.value, obj)
+ end,
+ ['index'] = function (obj)
+ Compile(obj.index, obj)
+ end,
+ ['select'] = function (obj)
+ local vararg = obj.vararg
+ if vararg.parent then
+ if not vararg.extParent then
+ vararg.extParent = {}
+ end
+ vararg.extParent[#vararg.extParent+1] = obj
+ else
+ Compile(vararg, obj)
+ end
+ end,
+ ['setname'] = function (obj)
+ Compile(obj.value, obj)
+ local loc = guide.getLocal(obj, obj[1], obj.start)
+ if loc then
+ obj.type = 'setlocal'
+ obj.loc = loc
+ addRef(loc, obj)
+ if loc.attrs then
+ local const
+ for i = 1, #loc.attrs do
+ local attr = loc.attrs[i][1]
+ if attr == 'const'
+ or attr == 'close' then
+ const = true
+ break
+ end
+ end
+ if const then
+ pushError {
+ type = 'SET_CONST',
+ start = obj.start,
+ finish = obj.finish,
+ }
+ end
+ end
+ else
+ obj.type = 'setglobal'
+ if ENVMode == '_ENV' then
+ local node = guide.getLocal(obj, '_ENV', obj.start)
+ if node then
+ addRef(node, obj)
+ end
+ end
+ end
+ end,
+ ['local'] = function (obj)
+ local attrs = obj.attrs
+ if attrs then
+ for i = 1, #attrs do
+ Compile(attrs[i], obj)
+ end
+ end
+ if Block then
+ if not Block.locals then
+ Block.locals = {}
+ end
+ Block.locals[#Block.locals+1] = obj
+ LocalCount = LocalCount + 1
+ if LocalCount > LocalLimit then
+ pushError {
+ type = 'LOCAL_LIMIT',
+ start = obj.start,
+ finish = obj.finish,
+ }
+ end
+ end
+ if obj.localfunction then
+ obj.localfunction = nil
+ end
+ Compile(obj.value, obj)
+ if obj.value and obj.value.special then
+ addSpecial(obj.value.special, obj)
+ end
+ end,
+ ['setfield'] = function (obj)
+ Compile(obj.node, obj)
+ Compile(obj.value, obj)
+ end,
+ ['do'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['return'] = function (obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ if Block and Block[#Block] ~= obj then
+ pushError {
+ type = 'ACTION_AFTER_RETURN',
+ start = obj.start,
+ finish = obj.finish,
+ }
+ end
+ local func = guide.getParentFunction(obj)
+ if func then
+ if not func.returns then
+ func.returns = {}
+ end
+ func.returns[#func.returns+1] = obj
+ end
+ end,
+ ['label'] = function (obj)
+ local block = guide.getBlock(obj)
+ if block then
+ if not block.labels then
+ block.labels = {}
+ end
+ local name = obj[1]
+ local label = guide.getLabel(block, name)
+ if label then
+ if Version == 'Lua 5.4'
+ or block == guide.getBlock(label) then
+ pushError {
+ type = 'REDEFINED_LABEL',
+ start = obj.start,
+ finish = obj.finish,
+ relative = {
+ {
+ label.start,
+ label.finish,
+ }
+ }
+ }
+ end
+ end
+ block.labels[name] = obj
+ end
+ end,
+ ['goto'] = function (obj)
+ GoToTag[#GoToTag+1] = obj
+ end,
+ ['if'] = function (obj)
+ for i = 1, #obj do
+ Compile(obj[i], obj)
+ end
+ end,
+ ['ifblock'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ Compile(obj.filter, obj)
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['elseifblock'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ Compile(obj.filter, obj)
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['elseblock'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['loop'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ Compile(obj.loc, obj)
+ Compile(obj.max, obj)
+ Compile(obj.step, obj)
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['in'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ local keys = obj.keys
+ for i = 1, #keys do
+ Compile(keys[i], obj)
+ end
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['while'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ Compile(obj.filter, obj)
+ CompileBlock(obj, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['repeat'] = function (obj)
+ local lastBlock = Block
+ Block = obj
+ CompileBlock(obj, obj)
+ Compile(obj.filter, obj)
+ if Block.locals then
+ LocalCount = LocalCount - #Block.locals
+ end
+ Block = lastBlock
+ end,
+ ['break'] = function (obj)
+ local block = guide.getBreakBlock(obj)
+ if block then
+ if not block.breaks then
+ block.breaks = {}
+ end
+ block.breaks[#block.breaks+1] = obj
+ else
+ pushError {
+ type = 'BREAK_OUTSIDE',
+ start = obj.start,
+ finish = obj.finish,
+ }
+ end
+ end,
+ ['main'] = function (obj)
+ Block = obj
+ if ENVMode == '_ENV' then
+ Compile({
+ type = 'local',
+ start = 0,
+ finish = 0,
+ effect = 0,
+ tag = '_ENV',
+ special= '_G',
+ [1] = '_ENV',
+ }, obj)
+ end
+ --- _ENV 是上值,不计入局部变量计数
+ LocalCount = 0
+ CompileBlock(obj, obj)
+ Block = nil
+ end,
+}
+
+function CompileBlock(obj, parent)
+ for i = 1, #obj do
+ local act = obj[i]
+ local f = vmMap[act.type]
+ if f then
+ act.parent = parent
+ f(act)
+ end
+ end
+end
+
+function Compile(obj, parent)
+ if not obj then
+ return nil
+ end
+ if Compiled[obj] then
+ return
+ end
+ Compiled[obj] = true
+ obj.parent = parent
+ local f = vmMap[obj.type]
+ if not f then
+ return
+ end
+ f(obj)
+end
+
+local function compileGoTo(obj)
+ local name = obj[1]
+ local label = guide.getLabel(obj, name)
+ if not label then
+ pushError {
+ type = 'NO_VISIBLE_LABEL',
+ start = obj.start,
+ finish = obj.finish,
+ info = {
+ label = name,
+ }
+ }
+ return
+ end
+ if not label.ref then
+ label.ref = {}
+ end
+ label.ref[#label.ref+1] = obj
+
+ -- 如果有局部变量在 goto 与 label 之间声明,
+ -- 并在 label 之后使用,则算作语法错误
+
+ -- 如果 label 在 goto 之前声明,那么不会有中间声明的局部变量
+ if obj.start > label.start then
+ return
+ end
+
+ local block = guide.getBlock(obj)
+ local locals = block and block.locals
+ if not locals then
+ return
+ end
+
+ for i = 1, #locals do
+ local loc = locals[i]
+ -- 检查局部变量声明位置为 goto 与 label 之间
+ if loc.start < obj.start or loc.finish > label.finish then
+ goto CONTINUE
+ end
+ -- 检查局部变量的使用位置在 label 之后
+ local refs = loc.ref
+ if not refs then
+ goto CONTINUE
+ end
+ for j = 1, #refs do
+ local ref = refs[j]
+ if ref.finish > label.finish then
+ pushError {
+ type = 'JUMP_LOCAL_SCOPE',
+ start = obj.start,
+ finish = obj.finish,
+ info = {
+ loc = loc[1],
+ },
+ relative = {
+ {
+ start = label.start,
+ finish = label.finish,
+ },
+ {
+ start = loc.start,
+ finish = loc.finish,
+ }
+ },
+ }
+ return
+ end
+ end
+ ::CONTINUE::
+ end
+end
+
+local function PostCompile()
+ for i = 1, #GoToTag do
+ compileGoTo(GoToTag[i])
+ end
+end
+
+return function (self, lua, mode, version)
+ local state, err = self:parse(lua, mode, version)
+ if not state then
+ return nil, err
+ end
+ pushError = state.pushError
+ if version == 'Lua 5.1' or version == 'LuaJIT' then
+ ENVMode = 'fenv'
+ else
+ ENVMode = '_ENV'
+ end
+ Compiled = {}
+ GoToTag = {}
+ LocalCount = 0
+ Version = version
+ Root = state.ast
+ if type(state.ast) == 'table' then
+ Compile(state.ast)
+ end
+ PostCompile()
+ Compiled = nil
+ GoToTag = nil
+ return state
+end
diff --git a/script-beta/src/parser/emmy.lua b/script-beta/src/parser/emmy.lua
new file mode 100644
index 00000000..4c1e087a
--- /dev/null
+++ b/script-beta/src/parser/emmy.lua
@@ -0,0 +1,321 @@
+local State
+local pushError
+
+local grammar = [[
+EmmyLua <- ({} '---' EmmyBody {} ShortComment)
+ -> EmmyLua
+EmmySp <- (!'---@' !'---' Comment / %s / %nl)*
+EmmyComments <- (EmmyComment (%nl EmmyComMulti / %nl EmmyComSingle)*)
+EmmyComment <- EmmySp %s* {(!%nl .)*}
+EmmyComMulti <- EmmySp '---|' {} -> en {(!%nl .)*}
+EmmyComSingle <- EmmySp '---' !'@' %s* {} -> ' ' {(!%nl .)*}
+EmmyBody <- '@class' %s+ EmmyClass -> EmmyClass
+ / '@type' %s+ EmmyType -> EmmyType
+ / '@alias' %s+ EmmyAlias -> EmmyAlias
+ / '@param' %s+ EmmyParam -> EmmyParam
+ / '@return' %s+ EmmyReturn -> EmmyReturn
+ / '@field' %s+ EmmyField -> EmmyField
+ / '@generic' %s+ EmmyGeneric -> EmmyGeneric
+ / '@vararg' %s+ EmmyVararg -> EmmyVararg
+ / '@language' %s+ EmmyLanguage -> EmmyLanguage
+ / '@see' %s+ EmmySee -> EmmySee
+ / '@overload' %s+ EmmyOverLoad -> EmmyOverLoad
+ / %s* EmmyComments -> EmmyComment
+ / EmmyIncomplete
+
+EmmyName <- ({} {[a-zA-Z_] [a-zA-Z0-9_]*})
+ -> EmmyName
+MustEmmyName <- EmmyName / DirtyEmmyName
+DirtyEmmyName <- {} -> DirtyEmmyName
+EmmyLongName <- ({} {(!%nl .)+})
+ -> EmmyName
+EmmyIncomplete <- MustEmmyName
+ -> EmmyIncomplete
+
+EmmyClass <- (MustEmmyName EmmyParentClass?)
+EmmyParentClass <- %s* {} ':' %s* MustEmmyName
+
+EmmyType <- EmmyTypeUnits EmmyTypeEnums
+EmmyTypeUnits <- {|
+ EmmyTypeUnit?
+ (%s* '|' %s* !String EmmyTypeUnit)*
+ |}
+EmmyTypeEnums <- {| EmmyTypeEnum* |}
+EmmyTypeUnit <- EmmyFunctionType
+ / EmmyTableType
+ / EmmyArrayType
+ / EmmyCommonType
+EmmyCommonType <- EmmyName
+ -> EmmyCommonType
+EmmyTypeEnum <- %s* (%nl %s* '---')? '|'? EmmyEnum
+ -> EmmyTypeEnum
+EmmyEnum <- %s* {'>'?} %s* String (EmmyEnumComment / (!%nl !'|' .)*)
+EmmyEnumComment <- %s* '#' %s* {(!%nl .)*}
+
+EmmyAlias <- MustEmmyName %s* EmmyType EmmyTypeEnum*
+
+EmmyParam <- MustEmmyName %s* EmmyType %s* EmmyOption %s* EmmyTypeEnum*
+EmmyOption <- Table?
+ -> EmmyOption
+
+EmmyReturn <- {} %nil {} Table -> EmmyOption
+ / {} EmmyType {} EmmyOption
+
+EmmyField <- (EmmyFieldAccess MustEmmyName %s* EmmyType)
+EmmyFieldAccess <- ({'public'} Cut %s*)
+ / ({'protected'} Cut %s*)
+ / ({'private'} Cut %s*)
+ / {} -> 'public'
+
+EmmyGeneric <- EmmyGenericBlock
+ (%s* ',' %s* EmmyGenericBlock)*
+EmmyGenericBlock<- (MustEmmyName %s* (':' %s* EmmyType)?)
+ -> EmmyGenericBlock
+
+EmmyVararg <- EmmyType
+
+EmmyLanguage <- MustEmmyName
+
+EmmyArrayType <- ({} MustEmmyName -> EmmyCommonType {} '[' DirtyBR)
+ -> EmmyArrayType
+ / ({} PL EmmyCommonType DirtyPR '[' DirtyBR)
+ -> EmmyArrayType
+
+EmmyTableType <- ({} 'table' Cut '<' %s* EmmyType %s* ',' %s* EmmyType %s* '>' {})
+ -> EmmyTableType
+
+EmmyFunctionType<- ({} 'fun' Cut %s* EmmyFunctionArgs %s* EmmyFunctionRtns {})
+ -> EmmyFunctionType
+EmmyFunctionArgs<- ('(' %s* EmmyFunctionArg %s* (',' %s* EmmyFunctionArg %s*)* DirtyPR)
+ -> EmmyFunctionArgs
+ / '(' %nil DirtyPR -> None
+ / %nil
+EmmyFunctionRtns<- (':' %s* EmmyType (%s* ',' %s* EmmyType)*)
+ -> EmmyFunctionRtns
+ / %nil
+EmmyFunctionArg <- MustEmmyName %s* ':' %s* EmmyType
+
+EmmySee <- {} MustEmmyName %s* '#' %s* MustEmmyName {}
+EmmyOverLoad <- EmmyFunctionType
+]]
+
+local ast = {
+ EmmyLua = function (start, emmy, finish)
+ emmy.start = start
+ emmy.finish = finish - 1
+ State.emmy[#State.emmy+1] = emmy
+ end,
+ EmmyName = function (start, str)
+ return {
+ type = 'name',
+ start = start,
+ finish = start + #str - 1,
+ [1] = str,
+ }
+ end,
+ DirtyEmmyName = function (pos)
+ pushError {
+ type = 'MISS_NAME',
+ level = 'warning',
+ start = pos,
+ finish = pos,
+ }
+ return {
+ type = 'emmyName',
+ start = pos-1,
+ finish = pos-1,
+ [1] = ''
+ }
+ end,
+ EmmyClass = function (class, startPos, extends)
+ if extends and extends[1] == '' then
+ extends.start = startPos
+ end
+ return {
+ type = 'class',
+ class = class,
+ extends = extends,
+ }
+ end,
+ EmmyType = function (types, enums)
+ local result = {
+ type = 'type',
+ types = types,
+ enums = enums,
+ }
+ return result
+ end,
+ EmmyCommonType = function (name)
+ return {
+ type = 'common',
+ start = name.start,
+ finish = name.finish,
+ name = name,
+ }
+ end,
+ EmmyArrayType = function (start, emmy, _, finish)
+ emmy.type = 'emmyArrayType'
+ emmy.start = start
+ emmy.finish = finish - 1
+ return emmy
+ end,
+ EmmyTableType = function (start, keyType, valueType, finish)
+ return {
+ type = 'emmyTableType',
+ start = start,
+ finish = finish - 1,
+ [1] = keyType,
+ [2] = valueType,
+ }
+ end,
+ EmmyFunctionType = function (start, args, returns, finish)
+ local result = {
+ start = start,
+ finish = finish - 1,
+ type = 'emmyFunctionType',
+ args = args,
+ returns = returns,
+ }
+ return result
+ end,
+ EmmyFunctionRtns = function (...)
+ return {...}
+ end,
+ EmmyFunctionArgs = function (...)
+ local args = {...}
+ args[#args] = nil
+ return args
+ end,
+ EmmyAlias = function (name, emmyName, ...)
+ return {
+ type = 'emmyAlias',
+ start = name.start,
+ finish = emmyName.finish,
+ name,
+ emmyName,
+ ...
+ }
+ end,
+ EmmyParam = function (argName, emmyName, option, ...)
+ local emmy = {
+ type = 'emmyParam',
+ option = option,
+ argName,
+ emmyName,
+ ...
+ }
+ emmy.start = emmy[1].start
+ emmy.finish = emmy[#emmy].finish
+ return emmy
+ end,
+ EmmyReturn = function (start, type, finish, option)
+ local emmy = {
+ type = 'emmyReturn',
+ option = option,
+ start = start,
+ finish = finish - 1,
+ [1] = type,
+ }
+ return emmy
+ end,
+ EmmyField = function (access, fieldName, ...)
+ local obj = {
+ type = 'emmyField',
+ access, fieldName,
+ ...
+ }
+ obj.start = obj[2].start
+ obj.finish = obj[3].finish
+ return obj
+ end,
+ EmmyGenericBlock = function (genericName, parentName)
+ return {
+ start = genericName.start,
+ finish = parentName and parentName.finish or genericName.finish,
+ genericName,
+ parentName,
+ }
+ end,
+ EmmyGeneric = function (...)
+ local emmy = {
+ type = 'emmyGeneric',
+ ...
+ }
+ emmy.start = emmy[1].start
+ emmy.finish = emmy[#emmy].finish
+ return emmy
+ end,
+ EmmyVararg = function (typeName)
+ return {
+ type = 'emmyVararg',
+ start = typeName.start,
+ finish = typeName.finish,
+ typeName,
+ }
+ end,
+ EmmyLanguage = function (language)
+ return {
+ type = 'emmyLanguage',
+ start = language.start,
+ finish = language.finish,
+ language,
+ }
+ end,
+ EmmySee = function (start, className, methodName, finish)
+ return {
+ type = 'emmySee',
+ start = start,
+ finish = finish - 1,
+ className, methodName
+ }
+ end,
+ EmmyOverLoad = function (EmmyFunctionType)
+ EmmyFunctionType.type = 'emmyOverLoad'
+ return EmmyFunctionType
+ end,
+ EmmyIncomplete = function (emmyName)
+ emmyName.type = 'emmyIncomplete'
+ return emmyName
+ end,
+ EmmyComment = function (...)
+ return {
+ type = 'emmyComment',
+ [1] = table.concat({...}),
+ }
+ end,
+ EmmyOption = function (options)
+ if not options or options == '' then
+ return nil
+ end
+ local option = {}
+ for _, pair in ipairs(options) do
+ if pair.type == 'pair' then
+ local key = pair[1]
+ local value = pair[2]
+ if key.type == 'name' then
+ option[key[1]] = value[1]
+ end
+ end
+ end
+ return option
+ end,
+ EmmyTypeEnum = function (default, enum, comment)
+ enum.type = 'enum'
+ if default ~= '' then
+ enum.default = true
+ end
+ enum.comment = comment
+ return enum
+ end,
+}
+
+local function init(state)
+ State = state
+ pushError = state.pushError
+end
+
+return {
+ grammar = grammar,
+ ast = ast,
+ init = init,
+}
diff --git a/script-beta/src/parser/grammar.lua b/script-beta/src/parser/grammar.lua
new file mode 100644
index 00000000..fd699bd4
--- /dev/null
+++ b/script-beta/src/parser/grammar.lua
@@ -0,0 +1,537 @@
+local re = require 'parser.relabel'
+local m = require 'lpeglabel'
+local emmy = require 'parser.emmy'
+local ast = require 'parser.ast'
+
+local scriptBuf = ''
+local compiled = {}
+local defs = ast.defs
+
+-- goto 可以作为名字,合法性之后处理
+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,
+}
+
+defs.nl = (m.P'\r\n' + m.S'\r\n')
+defs.s = m.S' \t'
+defs.S = - defs.s
+defs.ea = '\a'
+defs.eb = '\b'
+defs.ef = '\f'
+defs.en = '\n'
+defs.er = '\r'
+defs.et = '\t'
+defs.ev = '\v'
+defs['nil'] = m.Cp() / function () return nil end
+defs['false'] = m.Cp() / function () return false end
+defs.NotReserved = function (_, _, str)
+ if RESERVED[str] then
+ return false
+ end
+ return true
+end
+defs.Reserved = function (_, _, str)
+ if RESERVED[str] then
+ return true
+ end
+ return false
+end
+defs.None = function () end
+defs.np = m.Cp() / function (n) return n+1 end
+
+m.setmaxstack(1000)
+
+local eof = re.compile '!. / %{SYNTAX_ERROR}'
+
+local function grammar(tag)
+ return function (script)
+ scriptBuf = script .. '\r\n' .. scriptBuf
+ compiled[tag] = re.compile(scriptBuf, defs) * eof
+ end
+end
+
+local function errorpos(pos, err)
+ return {
+ type = 'UNKNOWN',
+ start = pos or 0,
+ finish = pos or 0,
+ err = err,
+ }
+end
+
+grammar 'Comment' [[
+Comment <- LongComment
+ / '--' ShortComment
+LongComment <- ('--[' {} {:eq: '='* :} {} '['
+ {(!CommentClose .)*}
+ (CommentClose / {}))
+ -> LongComment
+ / (
+ {} '/*' {}
+ (!'*/' .)*
+ {} '*/' {}
+ )
+ -> CLongComment
+CommentClose <- ']' =eq ']'
+ShortComment <- (!%nl .)*
+]]
+
+grammar 'Sp' [[
+Sp <- (EmmyLua / Comment / %nl / %s)*
+Sps <- (EmmyLua / Comment / %nl / %s)+
+
+-- 占位
+EmmyLua <- !. .
+]]
+
+grammar 'Common' [[
+Word <- [a-zA-Z0-9_]
+Cut <- !Word
+X16 <- [a-fA-F0-9]
+Rest <- (!%nl .)*
+
+AND <- Sp {'and'} Cut
+BREAK <- Sp 'break' Cut
+FALSE <- Sp 'false' Cut
+GOTO <- Sp 'goto' Cut
+LOCAL <- Sp 'local' Cut
+NIL <- Sp 'nil' Cut
+NOT <- Sp 'not' Cut
+OR <- Sp {'or'} Cut
+RETURN <- Sp 'return' Cut
+TRUE <- Sp 'true' Cut
+
+DO <- Sp {} 'do' {} Cut
+ / Sp({} 'then' {} Cut) -> ErrDo
+IF <- Sp {} 'if' {} Cut
+ELSE <- Sp {} 'else' {} Cut
+ELSEIF <- Sp {} 'elseif' {} Cut
+END <- Sp {} 'end' {} Cut
+FOR <- Sp {} 'for' {} Cut
+FUNCTION <- Sp {} 'function' {} Cut
+IN <- Sp {} 'in' {} Cut
+REPEAT <- Sp {} 'repeat' {} Cut
+THEN <- Sp {} 'then' {} Cut
+ / Sp({} 'do' {} Cut) -> ErrThen
+UNTIL <- Sp {} 'until' {} Cut
+WHILE <- Sp {} 'while' {} Cut
+
+
+Esc <- '\' -> ''
+ EChar
+EChar <- 'a' -> ea
+ / 'b' -> eb
+ / 'f' -> ef
+ / 'n' -> en
+ / 'r' -> er
+ / 't' -> et
+ / 'v' -> ev
+ / '\'
+ / '"'
+ / "'"
+ / %nl
+ / ('z' (%nl / %s)*) -> ''
+ / ({} 'x' {X16 X16}) -> Char16
+ / ([0-9] [0-9]? [0-9]?) -> Char10
+ / ('u{' {} {Word*} '}') -> CharUtf8
+ -- 错误处理
+ / 'x' {} -> MissEscX
+ / 'u' !'{' {} -> MissTL
+ / 'u{' Word* !'}' {} -> MissTR
+ / {} -> ErrEsc
+
+BOR <- Sp {'|'}
+BXOR <- Sp {'~'} !'='
+BAND <- Sp {'&'}
+Bshift <- Sp {BshiftList}
+BshiftList <- '<<'
+ / '>>'
+Concat <- Sp {'..'}
+Adds <- Sp {AddsList}
+AddsList <- '+'
+ / '-'
+Muls <- Sp {MulsList}
+MulsList <- '*'
+ / '//'
+ / '/'
+ / '%'
+Unary <- Sp {} {UnaryList}
+UnaryList <- NOT
+ / '#'
+ / '-'
+ / '~' !'='
+POWER <- Sp {'^'}
+
+BinaryOp <-( Sp {} {'or'} Cut
+ / Sp {} {'and'} Cut
+ / Sp {} {'<=' / '>=' / '<'!'<' / '>'!'>' / '~=' / '=='}
+ / Sp {} ({} '=' {}) -> ErrEQ
+ / Sp {} ({} '!=' {}) -> ErrUEQ
+ / Sp {} {'|'}
+ / Sp {} {'~'}
+ / Sp {} {'&'}
+ / Sp {} {'<<' / '>>'}
+ / Sp {} {'..'} !'.'
+ / Sp {} {'+' / '-'}
+ / Sp {} {'*' / '//' / '/' / '%'}
+ / Sp {} {'^'}
+ )-> BinaryOp
+UnaryOp <-( Sp {} {'not' Cut / '#' / '~' !'=' / '-' !'-'}
+ )-> UnaryOp
+
+PL <- Sp '('
+PR <- Sp ')'
+BL <- Sp '[' !'[' !'='
+BR <- Sp ']'
+TL <- Sp '{'
+TR <- Sp '}'
+COMMA <- Sp ({} ',')
+ -> COMMA
+SEMICOLON <- Sp ({} ';')
+ -> SEMICOLON
+DOTS <- Sp ({} '...')
+ -> DOTS
+DOT <- Sp ({} '.' !'.')
+ -> DOT
+COLON <- Sp ({} ':' !':')
+ -> COLON
+LABEL <- Sp '::'
+ASSIGN <- Sp '=' !'='
+AssignOrEQ <- Sp ({} '==' {})
+ -> ErrAssign
+ / Sp '='
+
+DirtyBR <- BR / {} -> MissBR
+DirtyTR <- TR / {} -> MissTR
+DirtyPR <- PR / {} -> MissPR
+DirtyLabel <- LABEL / {} -> MissLabel
+NeedEnd <- END / {} -> MissEnd
+NeedDo <- DO / {} -> MissDo
+NeedAssign <- ASSIGN / {} -> MissAssign
+NeedComma <- COMMA / {} -> MissComma
+NeedIn <- IN / {} -> MissIn
+NeedUntil <- UNTIL / {} -> MissUntil
+NeedThen <- THEN / {} -> MissThen
+]]
+
+grammar 'Nil' [[
+Nil <- Sp ({} -> Nil) NIL
+]]
+
+grammar 'Boolean' [[
+Boolean <- Sp ({} -> True) TRUE
+ / Sp ({} -> False) FALSE
+]]
+
+grammar 'String' [[
+String <- Sp ({} StringDef {})
+ -> String
+StringDef <- {'"'}
+ {~(Esc / !%nl !'"' .)*~} -> 1
+ ('"' / {} -> MissQuote1)
+ / {"'"}
+ {~(Esc / !%nl !"'" .)*~} -> 1
+ ("'" / {} -> MissQuote2)
+ / ('[' {} {:eq: '='* :} {} '[' %nl?
+ {(!StringClose .)*} -> 1
+ (StringClose / {}))
+ -> LongString
+StringClose <- ']' =eq ']'
+]]
+
+grammar 'Number' [[
+Number <- Sp ({} {NumberDef} {}) -> Number
+ NumberSuffix?
+ ErrNumber?
+NumberDef <- Number16 / Number10
+NumberSuffix<- ({} {[uU]? [lL] [lL]}) -> FFINumber
+ / ({} {[iI]}) -> ImaginaryNumber
+ErrNumber <- ({} {([0-9a-zA-Z] / '.')+}) -> UnknownSymbol
+
+Number10 <- Float10 Float10Exp?
+ / Integer10 Float10? Float10Exp?
+Integer10 <- [0-9]+ ('.' [0-9]*)?
+Float10 <- '.' [0-9]+
+Float10Exp <- [eE] [+-]? [0-9]+
+ / ({} [eE] [+-]? {}) -> MissExponent
+
+Number16 <- '0' [xX] Float16 Float16Exp?
+ / '0' [xX] Integer16 Float16? Float16Exp?
+Integer16 <- X16+ ('.' X16*)?
+ / ({} {Word*}) -> MustX16
+Float16 <- '.' X16+
+ / '.' ({} {Word*}) -> MustX16
+Float16Exp <- [pP] [+-]? [0-9]+
+ / ({} [pP] [+-]? {}) -> MissExponent
+]]
+
+grammar 'Name' [[
+Name <- Sp ({} NameBody {})
+ -> Name
+NameBody <- {[a-zA-Z_] [a-zA-Z0-9_]*}
+FreeName <- Sp ({} {NameBody=>NotReserved} {})
+ -> Name
+KeyWord <- Sp NameBody=>Reserved
+MustName <- Name / DirtyName
+DirtyName <- {} -> DirtyName
+]]
+
+grammar 'Exp' [[
+Exp <- (UnUnit BinUnit*)
+ -> Binary
+BinUnit <- (BinaryOp UnUnit?)
+ -> SubBinary
+UnUnit <- ExpUnit
+ / (UnaryOp+ (ExpUnit / MissExp))
+ -> Unary
+ExpUnit <- Nil
+ / Boolean
+ / String
+ / Number
+ / Dots
+ / Table
+ / Function
+ / Simple
+
+Simple <- {| Prefix (Sp Suffix)* |}
+ -> Simple
+Prefix <- Sp ({} PL DirtyExp DirtyPR {})
+ -> Paren
+ / Single
+Single <- FreeName
+ -> Single
+Suffix <- SuffixWithoutCall
+ / ({} PL SuffixCall DirtyPR {})
+ -> Call
+SuffixCall <- Sp ({} {| (COMMA / Exp)+ |} {})
+ -> PackExpList
+ / %nil
+SuffixWithoutCall
+ <- (DOT (Name / MissField))
+ -> GetField
+ / ({} BL DirtyExp DirtyBR {})
+ -> GetIndex
+ / (COLON (Name / MissMethod) NeedCall)
+ -> GetMethod
+ / ({} {| Table |} {})
+ -> Call
+ / ({} {| String |} {})
+ -> Call
+NeedCall <- (!(Sp CallStart) {} -> MissPL)?
+MissField <- {} -> MissField
+MissMethod <- {} -> MissMethod
+CallStart <- PL
+ / TL
+ / '"'
+ / "'"
+ / '[' '='* '['
+
+DirtyExp <- Exp
+ / {} -> DirtyExp
+MaybeExp <- Exp / MissExp
+MissExp <- {} -> MissExp
+ExpList <- Sp {| MaybeExp (Sp ',' MaybeExp)* |}
+
+Dots <- DOTS
+ -> VarArgs
+
+Table <- Sp ({} TL {| TableField* |} DirtyTR {})
+ -> Table
+TableField <- COMMA
+ / SEMICOLON
+ / NewIndex
+ / NewField
+ / Exp
+Index <- BL DirtyExp DirtyBR
+NewIndex <- Sp ({} Index NeedAssign DirtyExp {})
+ -> NewIndex
+NewField <- Sp ({} MustName ASSIGN DirtyExp {})
+ -> NewField
+
+Function <- FunctionBody
+ -> Function
+FuncArgs <- Sp ({} PL {| FuncArg+ |} DirtyPR {})
+ -> FuncArgs
+ / PL DirtyPR %nil
+ / {} -> MissPL DirtyPR %nil
+FuncArg <- DOTS
+ / Name
+ / COMMA
+FunctionBody<- FUNCTION FuncArgs
+ {| (!END Action)* |}
+ NeedEnd
+
+-- 纯占位,修改了 `relabel.lua` 使重复定义不抛错
+Action <- !END .
+]]
+
+grammar 'Action' [[
+Action <- Sp (CrtAction / UnkAction)
+CrtAction <- Semicolon
+ / Do
+ / Break
+ / Return
+ / Label
+ / GoTo
+ / If
+ / For
+ / While
+ / Repeat
+ / NamedFunction
+ / LocalFunction
+ / Local
+ / Set
+ / Call
+ / ExpInAction
+UnkAction <- ({} {Word+})
+ -> UnknownAction
+ / ({} '//' {} (LongComment / ShortComment))
+ -> CCommentPrefix
+ / ({} {. (!Sps !CrtAction .)*})
+ -> UnknownAction
+ExpInAction <- Sp ({} Exp {})
+ -> ExpInAction
+
+Semicolon <- Sp ';'
+SimpleList <- {| Simple (Sp ',' Simple)* |}
+
+Do <- Sp ({}
+ 'do' Cut
+ {| (!END Action)* |}
+ NeedEnd)
+ -> Do
+
+Break <- Sp ({} BREAK {})
+ -> Break
+
+Return <- Sp ({} RETURN ReturnExpList {})
+ -> Return
+ReturnExpList
+ <- Sp {| Exp (Sp ',' MaybeExp)* |}
+ / Sp {| !Exp !',' |}
+ / ExpList
+
+Label <- Sp ({} LABEL MustName DirtyLabel {})
+ -> Label
+
+GoTo <- Sp ({} GOTO MustName {})
+ -> GoTo
+
+If <- Sp ({} {| IfHead IfBody* |} NeedEnd)
+ -> If
+
+IfHead <- Sp (IfPart {}) -> IfBlock
+ / Sp (ElseIfPart {}) -> ElseIfBlock
+ / Sp (ElsePart {}) -> ElseBlock
+IfBody <- Sp (ElseIfPart {}) -> ElseIfBlock
+ / Sp (ElsePart {}) -> ElseBlock
+IfPart <- IF DirtyExp NeedThen
+ {| (!ELSEIF !ELSE !END Action)* |}
+ElseIfPart <- ELSEIF DirtyExp NeedThen
+ {| (!ELSEIF !ELSE !END Action)* |}
+ElsePart <- ELSE
+ {| (!ELSEIF !ELSE !END Action)* |}
+
+For <- Loop / In
+
+Loop <- LoopBody
+ -> Loop
+LoopBody <- FOR LoopArgs NeedDo
+ {} {| (!END Action)* |}
+ NeedEnd
+LoopArgs <- MustName AssignOrEQ
+ ({} {| (COMMA / !DO !END Exp)* |} {})
+ -> PackLoopArgs
+
+In <- InBody
+ -> In
+InBody <- FOR InNameList NeedIn InExpList NeedDo
+ {} {| (!END Action)* |}
+ NeedEnd
+InNameList <- ({} {| (COMMA / !IN !DO !END Name)* |} {})
+ -> PackInNameList
+InExpList <- ({} {| (COMMA / !DO !DO !END Exp)* |} {})
+ -> PackInExpList
+
+While <- WhileBody
+ -> While
+WhileBody <- WHILE DirtyExp NeedDo
+ {| (!END Action)* |}
+ NeedEnd
+
+Repeat <- (RepeatBody {})
+ -> Repeat
+RepeatBody <- REPEAT
+ {| (!UNTIL Action)* |}
+ NeedUntil DirtyExp
+
+LocalAttr <- {| (Sp '<' Sp MustName Sp LocalAttrEnd)+ |}
+ -> LocalAttr
+LocalAttrEnd<- '>' / {} -> MissGT
+Local <- Sp ({} LOCAL LocalNameList ((AssignOrEQ ExpList) / %nil) {})
+ -> Local
+Set <- Sp ({} SimpleList AssignOrEQ ExpList {})
+ -> Set
+LocalNameList
+ <- {| LocalName (Sp ',' LocalName)* |}
+LocalName <- (MustName LocalAttr?)
+ -> LocalName
+
+Call <- Simple
+ -> SimpleCall
+
+LocalFunction
+ <- Sp ({} LOCAL FunctionNamedBody)
+ -> LocalFunction
+
+NamedFunction
+ <- FunctionNamedBody
+ -> NamedFunction
+FunctionNamedBody
+ <- FUNCTION FuncName FuncArgs
+ {| (!END Action)* |}
+ NeedEnd
+FuncName <- {| Single (Sp SuffixWithoutCall)* |}
+ -> Simple
+ / {} -> MissName %nil
+]]
+
+--grammar 'EmmyLua' (emmy.grammar)
+
+grammar 'Lua' [[
+Lua <- Head?
+ ({} {| Action* |} {}) -> Lua
+ Sp
+Head <- '#' (!%nl .)*
+]]
+
+return function (self, lua, mode)
+ local gram = compiled[mode] or compiled['Lua']
+ local r, _, pos = gram:match(lua)
+ if not r then
+ local err = errorpos(pos)
+ return nil, err
+ end
+
+ return r
+end
diff --git a/script-beta/src/parser/guide.lua b/script-beta/src/parser/guide.lua
new file mode 100644
index 00000000..af511555
--- /dev/null
+++ b/script-beta/src/parser/guide.lua
@@ -0,0 +1,621 @@
+local error = error
+local type = type
+local next = next
+local tostring = tostring
+
+_ENV = nil
+
+local m = {}
+
+local blockTypes = {
+ ['while'] = true,
+ ['in'] = true,
+ ['loop'] = true,
+ ['repeat'] = true,
+ ['do'] = true,
+ ['function'] = true,
+ ['ifblock'] = true,
+ ['elseblock'] = true,
+ ['elseifblock'] = true,
+ ['main'] = true,
+}
+
+local breakBlockTypes = {
+ ['while'] = true,
+ ['in'] = true,
+ ['loop'] = true,
+ ['repeat'] = true,
+}
+
+m.childMap = {
+ ['main'] = {'#'},
+ ['repeat'] = {'#', 'filter'},
+ ['while'] = {'filter', '#'},
+ ['in'] = {'keys', '#'},
+ ['loop'] = {'loc', 'max', 'step', '#'},
+ ['if'] = {'#'},
+ ['ifblock'] = {'filter', '#'},
+ ['elseifblock'] = {'filter', '#'},
+ ['elseblock'] = {'#'},
+ ['setfield'] = {'node', 'field', 'value'},
+ ['setglobal'] = {'value'},
+ ['local'] = {'attrs', 'value'},
+ ['setlocal'] = {'value'},
+ ['return'] = {'#'},
+ ['do'] = {'#'},
+ ['select'] = {'vararg'},
+ ['table'] = {'#'},
+ ['tableindex'] = {'index', 'value'},
+ ['tablefield'] = {'field', 'value'},
+ ['function'] = {'args', '#'},
+ ['funcargs'] = {'#'},
+ ['setmethod'] = {'node', 'method', 'value'},
+ ['getmethod'] = {'node', 'method'},
+ ['setindex'] = {'node', 'index', 'value'},
+ ['getindex'] = {'node', 'index'},
+ ['paren'] = {'exp'},
+ ['call'] = {'node', 'args'},
+ ['callargs'] = {'#'},
+ ['getfield'] = {'node', 'field'},
+ ['list'] = {'#'},
+ ['binary'] = {1, 2},
+ ['unary'] = {1}
+}
+
+m.actionMap = {
+ ['main'] = {'#'},
+ ['repeat'] = {'#'},
+ ['while'] = {'#'},
+ ['in'] = {'#'},
+ ['loop'] = {'#'},
+ ['if'] = {'#'},
+ ['ifblock'] = {'#'},
+ ['elseifblock'] = {'#'},
+ ['elseblock'] = {'#'},
+ ['do'] = {'#'},
+ ['function'] = {'#'},
+ ['funcargs'] = {'#'},
+}
+
+--- 是否是字面量
+function m.isLiteral(obj)
+ local tp = obj.type
+ return tp == 'nil'
+ or tp == 'boolean'
+ or tp == 'string'
+ or tp == 'number'
+ or tp == 'table'
+end
+
+--- 获取字面量
+function m.getLiteral(obj)
+ local tp = obj.type
+ if tp == 'boolean' then
+ return obj[1]
+ elseif tp == 'string' then
+ return obj[1]
+ elseif tp == 'number' then
+ return obj[1]
+ end
+ return nil
+end
+
+--- 寻找父函数
+function m.getParentFunction(obj)
+ for _ = 1, 1000 do
+ obj = obj.parent
+ if not obj then
+ break
+ end
+ local tp = obj.type
+ if tp == 'function' or tp == 'main' then
+ return obj
+ end
+ end
+ return nil
+end
+
+--- 寻找所在区块
+function m.getBlock(obj)
+ for _ = 1, 1000 do
+ if not obj then
+ return nil
+ end
+ local tp = obj.type
+ if blockTypes[tp] then
+ return obj
+ end
+ obj = obj.parent
+ end
+ error('guide.getBlock overstack')
+end
+
+--- 寻找所在父区块
+function m.getParentBlock(obj)
+ for _ = 1, 1000 do
+ obj = obj.parent
+ if not obj then
+ return nil
+ end
+ local tp = obj.type
+ if blockTypes[tp] then
+ return obj
+ end
+ end
+ error('guide.getParentBlock overstack')
+end
+
+--- 寻找所在可break的父区块
+function m.getBreakBlock(obj)
+ for _ = 1, 1000 do
+ obj = obj.parent
+ if not obj then
+ return nil
+ end
+ local tp = obj.type
+ if breakBlockTypes[tp] then
+ return obj
+ end
+ if tp == 'function' then
+ return nil
+ end
+ end
+ error('guide.getBreakBlock overstack')
+end
+
+--- 寻找根区块
+function m.getRoot(obj)
+ for _ = 1, 1000 do
+ local parent = obj.parent
+ if not parent then
+ return obj
+ end
+ obj = parent
+ end
+ error('guide.getRoot overstack')
+end
+
+--- 寻找函数的不定参数,返回不定参在第几个参数上,以及该参数对象。
+--- 如果函数是主函数,则返回`0, nil`。
+---@return table
+---@return integer
+function m.getFunctionVarArgs(func)
+ if func.type == 'main' then
+ return 0, nil
+ end
+ if func.type ~= 'function' then
+ return nil, nil
+ end
+ local args = func.args
+ if not args then
+ return nil, nil
+ end
+ for i = 1, #args do
+ local arg = args[i]
+ if arg.type == '...' then
+ return i, arg
+ end
+ end
+ return nil, nil
+end
+
+--- 获取指定区块中可见的局部变量
+---@param block table
+---@param name string {comment = '变量名'}
+---@param pos integer {comment = '可见位置'}
+function m.getLocal(block, name, pos)
+ block = m.getBlock(block)
+ for _ = 1, 1000 do
+ if not block then
+ return nil
+ end
+ local locals = block.locals
+ local res
+ if not locals then
+ goto CONTINUE
+ end
+ for i = 1, #locals do
+ local loc = locals[i]
+ if loc.effect > pos then
+ break
+ end
+ if loc[1] == name then
+ if not res or res.effect < loc.effect then
+ res = loc
+ end
+ end
+ end
+ if res then
+ return res, res
+ end
+ ::CONTINUE::
+ block = m.getParentBlock(block)
+ end
+ error('guide.getLocal overstack')
+end
+
+--- 获取指定区块中可见的标签
+---@param block table
+---@param name string {comment = '标签名'}
+function m.getLabel(block, name)
+ block = m.getBlock(block)
+ for _ = 1, 1000 do
+ if not block then
+ return nil
+ end
+ local labels = block.labels
+ if labels then
+ local label = labels[name]
+ if label then
+ return label
+ end
+ end
+ if block.type == 'function' then
+ return nil
+ end
+ block = m.getParentBlock(block)
+ end
+ error('guide.getLocal overstack')
+end
+
+--- 判断source是否包含offset
+function m.isContain(source, offset)
+ return source.start <= offset and source.finish >= offset - 1
+end
+
+--- 判断offset在source的影响范围内
+---
+--- 主要针对赋值等语句时,key包含value
+function m.isInRange(source, offset)
+ return (source.vstart or source.start) <= offset and (source.range or source.finish) >= offset - 1
+end
+
+--- 添加child
+function m.addChilds(list, obj, map)
+ local keys = map[obj.type]
+ if keys then
+ for i = 1, #keys do
+ local key = keys[i]
+ if key == '#' then
+ for i = 1, #obj do
+ list[#list+1] = obj[i]
+ end
+ else
+ list[#list+1] = obj[key]
+ end
+ end
+ end
+end
+
+--- 遍历所有包含offset的source
+function m.eachSourceContain(ast, offset, callback)
+ local list = { ast }
+ while true do
+ local len = #list
+ if len == 0 then
+ return
+ end
+ local obj = list[len]
+ list[len] = nil
+ if m.isInRange(obj, offset) then
+ if m.isContain(obj, offset) then
+ local res = callback(obj)
+ if res ~= nil then
+ return res
+ end
+ end
+ m.addChilds(list, obj, m.childMap)
+ end
+ end
+end
+
+--- 遍历所有指定类型的source
+function m.eachSourceType(ast, type, callback)
+ local cache = ast.typeCache
+ if not cache then
+ local mark = {}
+ cache = {}
+ ast.typeCache = cache
+ m.eachSource(ast, function (source)
+ if mark[source] then
+ return
+ end
+ mark[source] = true
+ local tp = source.type
+ if not tp then
+ return
+ end
+ local myCache = cache[tp]
+ if not myCache then
+ myCache = {}
+ cache[tp] = myCache
+ end
+ myCache[#myCache+1] = source
+ end)
+ end
+ local myCache = cache[type]
+ if not myCache then
+ return
+ end
+ for i = 1, #myCache do
+ callback(myCache[i])
+ end
+end
+
+--- 遍历所有的source
+function m.eachSource(ast, callback)
+ local list = { ast }
+ while true do
+ local len = #list
+ if len == 0 then
+ return
+ end
+ local obj = list[len]
+ list[len] = nil
+ callback(obj)
+ m.addChilds(list, obj, m.childMap)
+ end
+end
+
+--- 获取指定的 special
+function m.eachSpecialOf(ast, name, callback)
+ local root = m.getRoot(ast)
+ if not root.specials then
+ return
+ end
+ local specials = root.specials[name]
+ if not specials then
+ return
+ end
+ for i = 1, #specials do
+ callback(specials[i])
+ end
+end
+
+--- 获取偏移对应的坐标
+---@param lines table
+---@return integer {name = 'row'}
+---@return integer {name = 'col'}
+function m.positionOf(lines, offset)
+ if offset < 1 then
+ return 0, 0
+ end
+ local lastLine = lines[#lines]
+ if offset > lastLine.finish then
+ return #lines, lastLine.finish - lastLine.start + 1
+ end
+ local min = 1
+ local max = #lines
+ for _ = 1, 100 do
+ if max <= min then
+ local line = lines[min]
+ return min, offset - line.start + 1
+ end
+ local row = (max - min) // 2 + min
+ local line = lines[row]
+ if offset < line.start then
+ max = row - 1
+ elseif offset > line.finish then
+ min = row + 1
+ else
+ return row, offset - line.start + 1
+ end
+ end
+ error('Stack overflow!')
+end
+
+--- 获取坐标对应的偏移
+---@param lines table
+---@param row integer
+---@param col integer
+---@return integer {name = 'offset'}
+function m.offsetOf(lines, row, col)
+ if row < 1 then
+ return 0
+ end
+ if row > #lines then
+ local lastLine = lines[#lines]
+ return lastLine.finish
+ end
+ local line = lines[row]
+ local len = line.finish - line.start + 1
+ if col < 0 then
+ return line.start
+ elseif col > len then
+ return line.finish
+ else
+ return line.start + col - 1
+ end
+end
+
+function m.lineContent(lines, text, row)
+ local line = lines[row]
+ if not line then
+ return ''
+ end
+ return text:sub(line.start, line.finish)
+end
+
+function m.lineRange(lines, row)
+ local line = lines[row]
+ if not line then
+ return 0, 0
+ end
+ return line.start, line.finish
+end
+
+function m.getName(obj)
+ local tp = obj.type
+ if tp == 'getglobal'
+ or tp == 'setglobal' then
+ return obj[1]
+ elseif tp == 'local'
+ or tp == 'getlocal'
+ or tp == 'setlocal' then
+ return obj[1]
+ elseif tp == 'getfield'
+ or tp == 'setfield'
+ or tp == 'tablefield' then
+ return obj.field[1]
+ elseif tp == 'getmethod'
+ or tp == 'setmethod' then
+ return obj.method[1]
+ elseif tp == 'getindex'
+ or tp == 'setindex'
+ or tp == 'tableindex' then
+ return m.getName(obj.index)
+ elseif tp == 'field'
+ or tp == 'method' then
+ return obj[1]
+ elseif tp == 'index' then
+ return m.getName(obj.index)
+ elseif tp == 'string' then
+ return obj[1]
+ end
+ return nil
+end
+
+function m.getKeyName(obj)
+ local tp = obj.type
+ if tp == 'getglobal'
+ or tp == 'setglobal' then
+ return 's|' .. obj[1]
+ elseif tp == 'getfield'
+ or tp == 'setfield'
+ or tp == 'tablefield' then
+ if obj.field then
+ return 's|' .. obj.field[1]
+ end
+ elseif tp == 'getmethod'
+ or tp == 'setmethod' then
+ if obj.method then
+ return 's|' .. obj.method[1]
+ end
+ elseif tp == 'getindex'
+ or tp == 'setindex'
+ or tp == 'tableindex' then
+ if obj.index then
+ return m.getKeyName(obj.index)
+ end
+ elseif tp == 'field'
+ or tp == 'method' then
+ return 's|' .. obj[1]
+ elseif tp == 'string' then
+ local s = obj[1]
+ if s then
+ return 's|' .. s
+ else
+ return s
+ end
+ elseif tp == 'number' then
+ local n = obj[1]
+ if n then
+ return ('n|%q'):format(obj[1])
+ else
+ return 'n'
+ end
+ elseif tp == 'boolean' then
+ local b = obj[1]
+ if b then
+ return 'b|' .. tostring(b)
+ else
+ return 'b'
+ end
+ end
+ return nil
+end
+
+function m.getENV(ast)
+ if ast.type ~= 'main' then
+ return nil
+ end
+ return ast.locals[1]
+end
+
+--- 测试 a 到 b 的路径(不经过函数,不考虑 goto),
+--- 每个路径是一个 block 。
+---
+--- 如果 a 在 b 的前面,返回 `"before"` 加上 2个`list<block>`
+---
+--- 如果 a 在 b 的后面,返回 `"after"` 加上 2个`list<block>`
+---
+--- 否则返回 `false`
+---
+--- 返回的2个 `list` 分别为基准block到达 a 与 b 的路径。
+---@param a table
+---@param b table
+---@return string|boolean mode
+---@return table|nil pathA
+---@return table|nil pathB
+function m.getPath(a, b)
+ --- 首先测试双方在同一个函数内
+ if m.getParentFunction(a) ~= m.getParentFunction(b) then
+ return false
+ end
+ local mode
+ local objA
+ local objB
+ if a.finish < b.start then
+ mode = 'before'
+ objA = a
+ objB = b
+ elseif a.start > b.finish then
+ mode = 'after'
+ objA = b
+ objB = a
+ else
+ return 'equal', {}, {}
+ end
+ local pathA = {}
+ local pathB = {}
+ for _ = 1, 1000 do
+ objA = m.getParentBlock(objA)
+ pathA[#pathA+1] = objA
+ if objA.type == 'function' or objA.type == 'main' then
+ break
+ end
+ end
+ for _ = 1, 1000 do
+ objB = m.getParentBlock(objB)
+ pathB[#pathB+1] = objB
+ if objB.type == 'function' or objB.type == 'main' then
+ break
+ end
+ end
+ -- pathA: {1, 2, 3, 4, 5}
+ -- pathB: {5, 6, 2, 3}
+ local top = #pathB
+ local start
+ for i = #pathA, 1, -1 do
+ local currentBlock = pathA[i]
+ if currentBlock == pathB[top] then
+ start = i
+ break
+ end
+ end
+ -- pathA: { 1, 2, 3}
+ -- pathB: {5, 6, 2, 3}
+ local extra = 0
+ local align = top - start
+ for i = start, 1, -1 do
+ local currentA = pathA[i]
+ local currentB = pathB[i+align]
+ if currentA ~= currentB then
+ extra = i
+ break
+ end
+ end
+ -- pathA: {1}
+ local resultA = {}
+ for i = extra, 1, -1 do
+ resultA[#resultA+1] = pathA[i]
+ end
+ -- pathB: {5, 6}
+ local resultB = {}
+ for i = extra + align, 1, -1 do
+ resultB[#resultB+1] = pathB[i]
+ end
+ return mode, resultA, resultB
+end
+
+return m
diff --git a/script-beta/src/parser/init.lua b/script-beta/src/parser/init.lua
new file mode 100644
index 00000000..5eeb0da2
--- /dev/null
+++ b/script-beta/src/parser/init.lua
@@ -0,0 +1,11 @@
+local api = {
+ grammar = require 'parser.grammar',
+ parse = require 'parser.parse',
+ compile = require 'parser.compile',
+ split = require 'parser.split',
+ calcline = require 'parser.calcline',
+ lines = require 'parser.lines',
+ guide = require 'parser.guide',
+}
+
+return api
diff --git a/script-beta/src/parser/lines.lua b/script-beta/src/parser/lines.lua
new file mode 100644
index 00000000..c7961d13
--- /dev/null
+++ b/script-beta/src/parser/lines.lua
@@ -0,0 +1,46 @@
+local m = require 'lpeglabel'
+local utf8Len = utf8.len
+
+_ENV = nil
+
+local function Line(start, line, range, finish)
+ line.start = start
+ line.finish = finish - 1
+ line.range = range - 1
+ return line
+end
+
+local function Space(...)
+ local line = {...}
+ local sp = 0
+ local tab = 0
+ for i = 1, #line do
+ if line[i] == ' ' then
+ sp = sp + 1
+ elseif line[i] == '\t' then
+ tab = tab + 1
+ end
+ line[i] = nil
+ end
+ line.sp = sp
+ line.tab = tab
+ return line
+end
+
+local parser = m.P{
+'Lines',
+Lines = m.Ct(m.V'Line'^0 * m.V'LastLine'),
+Line = m.Cp() * m.V'Indent' * (1 - m.V'Nl')^0 * m.Cp() * m.V'Nl' * m.Cp() / Line,
+LastLine= m.Cp() * m.V'Indent' * (1 - m.V'Nl')^0 * m.Cp() * m.Cp() / Line,
+Nl = m.P'\r\n' + m.S'\r\n',
+Indent = m.C(m.S' \t')^0 / Space,
+}
+
+return function (self, text)
+ local lines, err = parser:match(text)
+ if not lines then
+ return nil, err
+ end
+
+ return lines
+end
diff --git a/script-beta/src/parser/parse.lua b/script-beta/src/parser/parse.lua
new file mode 100644
index 00000000..bbc01b10
--- /dev/null
+++ b/script-beta/src/parser/parse.lua
@@ -0,0 +1,45 @@
+local ast = require 'parser.ast'
+
+return function (self, lua, mode, version)
+ local errs = {}
+ local diags = {}
+ local state = {
+ version = version,
+ lua = lua,
+ emmy = {},
+ root = {},
+ errs = errs,
+ diags = diags,
+ pushError = function (err)
+ if err.finish < err.start then
+ err.finish = err.start
+ end
+ local last = errs[#errs]
+ if last then
+ if last.start <= err.start and last.finish >= err.finish then
+ return
+ end
+ end
+ err.level = err.level or 'error'
+ errs[#errs+1] = err
+ return err
+ end,
+ pushDiag = function (code, info)
+ if not diags[code] then
+ diags[code] = {}
+ end
+ diags[code][#diags[code]+1] = info
+ end
+ }
+ ast.init(state)
+ local suc, res, err = xpcall(self.grammar, debug.traceback, self, lua, mode)
+ ast.close()
+ if not suc then
+ return nil, res
+ end
+ if not res then
+ state.pushError(err)
+ end
+ state.ast = res
+ return state
+end
diff --git a/script-beta/src/parser/relabel.lua b/script-beta/src/parser/relabel.lua
new file mode 100644
index 00000000..ac902403
--- /dev/null
+++ b/script-beta/src/parser/relabel.lua
@@ -0,0 +1,361 @@
+-- $Id: re.lua,v 1.44 2013/03/26 20:11:40 roberto Exp $
+
+-- imported functions and modules
+local tonumber, type, print, error = tonumber, type, print, error
+local pcall = pcall
+local setmetatable = setmetatable
+local tinsert, concat = table.insert, table.concat
+local rep = string.rep
+local m = require"lpeglabel"
+
+-- 'm' will be used to parse expressions, and 'mm' will be used to
+-- create expressions; that is, 're' runs on 'm', creating patterns
+-- on 'mm'
+local mm = m
+
+-- pattern's metatable
+local mt = getmetatable(mm.P(0))
+
+
+
+-- No more global accesses after this point
+_ENV = nil
+
+
+local any = m.P(1)
+local dummy = mm.P(false)
+
+
+local errinfo = {
+ NoPatt = "no pattern found",
+ ExtraChars = "unexpected characters after the pattern",
+
+ ExpPatt1 = "expected a pattern after '/'",
+
+ ExpPatt2 = "expected a pattern after '&'",
+ ExpPatt3 = "expected a pattern after '!'",
+
+ ExpPatt4 = "expected a pattern after '('",
+ ExpPatt5 = "expected a pattern after ':'",
+ ExpPatt6 = "expected a pattern after '{~'",
+ ExpPatt7 = "expected a pattern after '{|'",
+
+ ExpPatt8 = "expected a pattern after '<-'",
+
+ ExpPattOrClose = "expected a pattern or closing '}' after '{'",
+
+ ExpNumName = "expected a number, '+', '-' or a name (no space) after '^'",
+ ExpCap = "expected a string, number, '{}' or name after '->'",
+
+ ExpName1 = "expected the name of a rule after '=>'",
+ ExpName2 = "expected the name of a rule after '=' (no space)",
+ ExpName3 = "expected the name of a rule after '<' (no space)",
+
+ ExpLab1 = "expected a label after '{'",
+
+ ExpNameOrLab = "expected a name or label after '%' (no space)",
+
+ ExpItem = "expected at least one item after '[' or '^'",
+
+ MisClose1 = "missing closing ')'",
+ MisClose2 = "missing closing ':}'",
+ MisClose3 = "missing closing '~}'",
+ MisClose4 = "missing closing '|}'",
+ MisClose5 = "missing closing '}'", -- for the captures
+
+ MisClose6 = "missing closing '>'",
+ MisClose7 = "missing closing '}'", -- for the labels
+
+ MisClose8 = "missing closing ']'",
+
+ MisTerm1 = "missing terminating single quote",
+ MisTerm2 = "missing terminating double quote",
+}
+
+local function expect (pattern, label)
+ return pattern + m.T(label)
+end
+
+
+-- Pre-defined names
+local Predef = { nl = m.P"\n" }
+
+
+local mem
+local fmem
+local gmem
+
+
+local function updatelocale ()
+ mm.locale(Predef)
+ Predef.a = Predef.alpha
+ Predef.c = Predef.cntrl
+ Predef.d = Predef.digit
+ Predef.g = Predef.graph
+ Predef.l = Predef.lower
+ Predef.p = Predef.punct
+ Predef.s = Predef.space
+ Predef.u = Predef.upper
+ Predef.w = Predef.alnum
+ Predef.x = Predef.xdigit
+ Predef.A = any - Predef.a
+ Predef.C = any - Predef.c
+ Predef.D = any - Predef.d
+ Predef.G = any - Predef.g
+ Predef.L = any - Predef.l
+ Predef.P = any - Predef.p
+ Predef.S = any - Predef.s
+ Predef.U = any - Predef.u
+ Predef.W = any - Predef.w
+ Predef.X = any - Predef.x
+ mem = {} -- restart memoization
+ fmem = {}
+ gmem = {}
+ local mt = {__mode = "v"}
+ setmetatable(mem, mt)
+ setmetatable(fmem, mt)
+ setmetatable(gmem, mt)
+end
+
+
+updatelocale()
+
+
+
+local I = m.P(function (s,i) print(i, s:sub(1, i-1)); return i end)
+
+
+local function getdef (id, defs)
+ local c = defs and defs[id]
+ if not c then
+ error("undefined name: " .. id)
+ end
+ return c
+end
+
+
+local function mult (p, n)
+ local np = mm.P(true)
+ while n >= 1 do
+ if n%2 >= 1 then np = np * p end
+ p = p * p
+ n = n/2
+ end
+ return np
+end
+
+local function equalcap (s, i, c)
+ if type(c) ~= "string" then return nil end
+ local e = #c + i
+ if s:sub(i, e - 1) == c then return e else return nil end
+end
+
+
+local S = (Predef.space + "--" * (any - Predef.nl)^0)^0
+
+local name = m.C(m.R("AZ", "az", "__") * m.R("AZ", "az", "__", "09")^0)
+
+local arrow = S * "<-"
+
+-- a defined name only have meaning in a given environment
+local Def = name * m.Carg(1)
+
+local num = m.C(m.R"09"^1) * S / tonumber
+
+local String = "'" * m.C((any - "'" - m.P"\n")^0) * expect("'", "MisTerm1")
+ + '"' * m.C((any - '"' - m.P"\n")^0) * expect('"', "MisTerm2")
+
+
+local defined = "%" * Def / function (c,Defs)
+ local cat = Defs and Defs[c] or Predef[c]
+ if not cat then
+ error("name '" .. c .. "' undefined")
+ end
+ return cat
+end
+
+local Range = m.Cs(any * (m.P"-"/"") * (any - "]")) / mm.R
+
+local item = defined + Range + m.C(any - m.P"\n")
+
+local Class =
+ "["
+ * (m.C(m.P"^"^-1)) -- optional complement symbol
+ * m.Cf(expect(item, "ExpItem") * (item - "]")^0, mt.__add)
+ / function (c, p) return c == "^" and any - p or p end
+ * expect("]", "MisClose8")
+
+local function adddef (t, k, exp)
+ if t[k] then
+ -- TODO 改了一下这里的代码,重复定义不会抛错
+ --error("'"..k.."' already defined as a rule")
+ else
+ t[k] = exp
+ end
+ return t
+end
+
+local function firstdef (n, r) return adddef({n}, n, r) end
+
+
+local function NT (n, b)
+ if not b then
+ error("rule '"..n.."' used outside a grammar")
+ else return mm.V(n)
+ end
+end
+
+
+local exp = m.P{ "Exp",
+ Exp = S * ( m.V"Grammar"
+ + m.Cf(m.V"Seq" * (S * "/" * expect(S * m.V"Seq", "ExpPatt1"))^0, mt.__add) );
+ Seq = m.Cf(m.Cc(m.P"") * m.V"Prefix" * (S * m.V"Prefix")^0, mt.__mul);
+ Prefix = "&" * expect(S * m.V"Prefix", "ExpPatt2") / mt.__len
+ + "!" * expect(S * m.V"Prefix", "ExpPatt3") / mt.__unm
+ + m.V"Suffix";
+ Suffix = m.Cf(m.V"Primary" *
+ ( S * ( m.P"+" * m.Cc(1, mt.__pow)
+ + m.P"*" * m.Cc(0, mt.__pow)
+ + m.P"?" * m.Cc(-1, mt.__pow)
+ + "^" * expect( m.Cg(num * m.Cc(mult))
+ + m.Cg(m.C(m.S"+-" * m.R"09"^1) * m.Cc(mt.__pow)
+ + name * m.Cc"lab"
+ ),
+ "ExpNumName")
+ + "->" * expect(S * ( m.Cg((String + num) * m.Cc(mt.__div))
+ + m.P"{}" * m.Cc(nil, m.Ct)
+ + m.Cg(Def / getdef * m.Cc(mt.__div))
+ ),
+ "ExpCap")
+ + "=>" * expect(S * m.Cg(Def / getdef * m.Cc(m.Cmt)),
+ "ExpName1")
+ )
+ )^0, function (a,b,f) if f == "lab" then return a + mm.T(b) else return f(a,b) end end );
+ Primary = "(" * expect(m.V"Exp", "ExpPatt4") * expect(S * ")", "MisClose1")
+ + String / mm.P
+ + Class
+ + defined
+ + "%" * expect(m.P"{", "ExpNameOrLab")
+ * expect(S * m.V"Label", "ExpLab1")
+ * expect(S * "}", "MisClose7") / mm.T
+ + "{:" * (name * ":" + m.Cc(nil)) * expect(m.V"Exp", "ExpPatt5")
+ * expect(S * ":}", "MisClose2")
+ / function (n, p) return mm.Cg(p, n) end
+ + "=" * expect(name, "ExpName2")
+ / function (n) return mm.Cmt(mm.Cb(n), equalcap) end
+ + m.P"{}" / mm.Cp
+ + "{~" * expect(m.V"Exp", "ExpPatt6")
+ * expect(S * "~}", "MisClose3") / mm.Cs
+ + "{|" * expect(m.V"Exp", "ExpPatt7")
+ * expect(S * "|}", "MisClose4") / mm.Ct
+ + "{" * expect(m.V"Exp", "ExpPattOrClose")
+ * expect(S * "}", "MisClose5") / mm.C
+ + m.P"." * m.Cc(any)
+ + (name * -arrow + "<" * expect(name, "ExpName3")
+ * expect(">", "MisClose6")) * m.Cb("G") / NT;
+ Label = num + name;
+ Definition = name * arrow * expect(m.V"Exp", "ExpPatt8");
+ Grammar = m.Cg(m.Cc(true), "G")
+ * m.Cf(m.V"Definition" / firstdef * (S * m.Cg(m.V"Definition"))^0,
+ adddef) / mm.P;
+}
+
+local pattern = S * m.Cg(m.Cc(false), "G") * expect(exp, "NoPatt") / mm.P
+ * S * expect(-any, "ExtraChars")
+
+local function lineno (s, i)
+ if i == 1 then return 1, 1 end
+ local adjustment = 0
+ -- report the current line if at end of line, not the next
+ if s:sub(i,i) == '\n' then
+ i = i-1
+ adjustment = 1
+ end
+ local rest, num = s:sub(1,i):gsub("[^\n]*\n", "")
+ local r = #rest
+ return 1 + num, (r ~= 0 and r or 1) + adjustment
+end
+
+local function calcline (s, i)
+ if i == 1 then return 1, 1 end
+ local rest, line = s:sub(1,i):gsub("[^\n]*\n", "")
+ local col = #rest
+ return 1 + line, col ~= 0 and col or 1
+end
+
+
+local function splitlines(str)
+ local t = {}
+ local function helper(line) tinsert(t, line) return "" end
+ helper((str:gsub("(.-)\r?\n", helper)))
+ return t
+end
+
+local function compile (p, defs)
+ if mm.type(p) == "pattern" then return p end -- already compiled
+ p = p .. " " -- for better reporting of column numbers in errors when at EOF
+ local ok, cp, label, poserr = pcall(function() return pattern:match(p, 1, defs) end)
+ if not ok and cp then
+ if type(cp) == "string" then
+ cp = cp:gsub("^[^:]+:[^:]+: ", "")
+ end
+ error(cp, 3)
+ end
+ if not cp then
+ local lines = splitlines(p)
+ local line, col = lineno(p, poserr)
+ local err = {}
+ tinsert(err, "L" .. line .. ":C" .. col .. ": " .. errinfo[label])
+ tinsert(err, lines[line])
+ tinsert(err, rep(" ", col-1) .. "^")
+ error("syntax error(s) in pattern\n" .. concat(err, "\n"), 3)
+ end
+ return cp
+end
+
+local function match (s, p, i)
+ local cp = mem[p]
+ if not cp then
+ cp = compile(p)
+ mem[p] = cp
+ end
+ return cp:match(s, i or 1)
+end
+
+local function find (s, p, i)
+ local cp = fmem[p]
+ if not cp then
+ cp = compile(p) / 0
+ cp = mm.P{ mm.Cp() * cp * mm.Cp() + 1 * mm.V(1) }
+ fmem[p] = cp
+ end
+ local i, e = cp:match(s, i or 1)
+ if i then return i, e - 1
+ else return i
+ end
+end
+
+local function gsub (s, p, rep)
+ local g = gmem[p] or {} -- ensure gmem[p] is not collected while here
+ gmem[p] = g
+ local cp = g[rep]
+ if not cp then
+ cp = compile(p)
+ cp = mm.Cs((cp / rep + 1)^0)
+ g[rep] = cp
+ end
+ return cp:match(s)
+end
+
+
+-- exported names
+local re = {
+ compile = compile,
+ match = match,
+ find = find,
+ gsub = gsub,
+ updatelocale = updatelocale,
+ calcline = calcline
+}
+
+return re
diff --git a/script-beta/src/parser/split.lua b/script-beta/src/parser/split.lua
new file mode 100644
index 00000000..6ce4a4e7
--- /dev/null
+++ b/script-beta/src/parser/split.lua
@@ -0,0 +1,9 @@
+local m = require 'lpeglabel'
+
+local NL = m.P'\r\n' + m.S'\r\n'
+local LINE = m.C(1 - NL)
+
+return function (str)
+ local MATCH = m.Ct((LINE * NL)^0 * LINE)
+ return MATCH:match(str)
+end
diff --git a/script-beta/src/proto/define.lua b/script-beta/src/proto/define.lua
new file mode 100644
index 00000000..61c4037c
--- /dev/null
+++ b/script-beta/src/proto/define.lua
@@ -0,0 +1,140 @@
+local guide = require 'parser.guide'
+
+local m = {}
+
+--- 获取 position 对应的光标位置
+---@param lines table
+---@param text string
+---@param position position
+---@return integer
+function m.offset(lines, text, position)
+ local row = position.line + 1
+ local start = guide.lineRange(lines, row)
+ local offset = utf8.offset(text, position.character + 1, start)
+ if text:sub(offset-1, offset):match '[%w_][^%w_]' then
+ offset = offset - 1
+ end
+ return offset
+end
+
+--- 将光标位置转化为 position
+---@alias position table
+---@param lines table
+---@param text string
+---@param offset integer
+---@return position
+function m.position(lines, text, offset)
+ local row, col = guide.positionOf(lines, offset)
+ local start = guide.lineRange(lines, row)
+ if start < 1 then
+ start = 1
+ end
+ local ucol = utf8.len(text, start, start + col - 1, true)
+ if row < 1 then
+ row = 1
+ end
+ return {
+ line = row - 1,
+ character = ucol,
+ }
+end
+
+--- 将起点与终点位置转化为 range
+---@alias range table
+---@param lines table
+---@param text string
+---@param offset1 integer
+---@param offset2 integer
+function m.range(lines, text, offset1, offset2)
+ local range = {
+ start = m.position(lines, text, offset1),
+ ['end'] = m.position(lines, text, offset2),
+ }
+ if range.start.character > 0 then
+ range.start.character = range.start.character - 1
+ end
+ return range
+end
+
+---@alias location table
+---@param uri string
+---@param range range
+---@return location
+function m.location(uri, range)
+ return {
+ uri = uri,
+ range = range,
+ }
+end
+
+---@alias locationLink table
+---@param uri string
+---@param range range
+---@param selection range
+---@param origin range
+function m.locationLink(uri, range, selection, origin)
+ return {
+ targetUri = uri,
+ targetRange = range,
+ targetSelectionRange = selection,
+ originSelectionRange = origin,
+ }
+end
+
+function m.textEdit(range, newtext)
+ return {
+ range = range,
+ newText = newtext,
+ }
+end
+
+--- 诊断等级
+m.DiagnosticSeverity = {
+ Error = 1,
+ Warning = 2,
+ Information = 3,
+ Hint = 4,
+}
+
+--- 诊断类型与默认等级
+m.DiagnosticDefaultSeverity = {
+ ['unused-local'] = 'Hint',
+ ['unused-function'] = 'Hint',
+ ['undefined-global'] = 'Warning',
+ ['global-in-nil-env'] = 'Warning',
+ ['unused-label'] = 'Hint',
+ ['unused-vararg'] = 'Hint',
+ ['trailing-space'] = 'Hint',
+ ['redefined-local'] = 'Hint',
+ ['newline-call'] = 'Information',
+ ['newfield-call'] = 'Warning',
+ ['redundant-parameter'] = 'Hint',
+ ['ambiguity-1'] = 'Warning',
+ ['lowercase-global'] = 'Information',
+ ['undefined-env-child'] = 'Information',
+ ['duplicate-index'] = 'Warning',
+ ['empty-block'] = 'Hint',
+ ['redundant-value'] = 'Hint',
+ ['emmy-lua'] = 'Warning',
+}
+
+--- 诊断报告标签
+m.DiagnosticTag = {
+ Unnecessary = 1,
+ Deprecated = 2,
+}
+
+m.DocumentHighlightKind = {
+ Text = 1,
+ Read = 2,
+ Write = 3,
+}
+
+m.MessageType = {
+ Error = 1,
+ Warning = 2,
+ Info = 3,
+ Log = 4,
+}
+
+return m
diff --git a/script-beta/src/proto/init.lua b/script-beta/src/proto/init.lua
new file mode 100644
index 00000000..33e637f6
--- /dev/null
+++ b/script-beta/src/proto/init.lua
@@ -0,0 +1,3 @@
+local proto = require 'proto.proto'
+
+return proto
diff --git a/script-beta/src/proto/proto.lua b/script-beta/src/proto/proto.lua
new file mode 100644
index 00000000..f04653d5
--- /dev/null
+++ b/script-beta/src/proto/proto.lua
@@ -0,0 +1,133 @@
+local subprocess = require 'bee.subprocess'
+local util = require 'utility'
+local await = require 'await'
+local pub = require 'pub'
+local jsonrpc = require 'jsonrpc'
+local ErrorCodes = require 'define.ErrorCodes'
+
+local reqCounter = util.counter()
+
+local m = {}
+
+m.ability = {}
+m.waiting = {}
+
+function m.getMethodName(proto)
+ if proto.method:sub(1, 2) == '$/' then
+ return proto.method:sub(3), true
+ else
+ return proto.method, false
+ end
+end
+
+function m.on(method, callback)
+ m.ability[method] = callback
+end
+
+function m.response(id, res)
+ if id == nil then
+ log.error('Response id is nil!', util.dump(res))
+ return
+ end
+ -- res 可能是nil,为了转成json时保留nil,使用 container 容器
+ local data = util.container()
+ data.id = id
+ data.result = res
+ local buf = jsonrpc.encode(data)
+ log.debug('Response', id, #buf)
+ io.stdout:write(buf)
+end
+
+function m.responseErr(id, code, message)
+ if id == nil then
+ log.error('Response id is nil!', util.dump(message))
+ return
+ end
+ local buf = jsonrpc.encode {
+ id = id,
+ error = {
+ code = code,
+ message = message,
+ }
+ }
+ log.debug('ResponseErr', id, #buf)
+ io.stdout:write(buf)
+end
+
+function m.notify(name, params)
+ local buf = jsonrpc.encode {
+ method = name,
+ params = params,
+ }
+ log.debug('Notify', name, #buf)
+ io.stdout:write(buf)
+end
+
+function m.awaitRequest(name, params)
+ local id = reqCounter()
+ local buf = jsonrpc.encode {
+ id = id,
+ method = name,
+ params = params,
+ }
+ log.debug('Request', name, #buf)
+ io.stdout:write(buf)
+ return await.wait(function (waker)
+ m.waiting[id] = waker
+ end)
+end
+
+function m.doMethod(proto)
+ local method, optional = m.getMethodName(proto)
+ local abil = m.ability[method]
+ if not abil then
+ if not optional then
+ log.warn('Recieved unknown proto: ' .. method)
+ end
+ if proto.id then
+ m.responseErr(proto.id, ErrorCodes.MethodNotFound, method)
+ end
+ return
+ end
+ await.create(function ()
+ local clock = os.clock()
+ local ok, res = xpcall(abil, log.error, proto.params)
+ local passed = os.clock() - clock
+ if passed > 0.2 then
+ log.debug(('Method [%s] takes [%.3f]sec.'):format(method, passed))
+ end
+ if not proto.id then
+ return
+ end
+ if ok then
+ m.response(proto.id, res)
+ else
+ m.responseErr(proto.id, ErrorCodes.InternalError, res)
+ end
+ end)
+end
+
+function m.doResponse(proto)
+ local id = proto.id
+ local waker = m.waiting[id]
+ if not waker then
+ log.warn('Response id not found: ' .. util.dump(proto))
+ return
+ end
+ m.waiting[id] = nil
+ if proto.error then
+ log.warn(('Response error [%d]: %s'):format(proto.error.code, proto.error.message))
+ return
+ end
+ waker(proto.result)
+end
+
+function m.listen()
+ subprocess.filemode(io.stdin, 'b')
+ subprocess.filemode(io.stdout, 'b')
+ io.stdin:setvbuf 'no'
+ io.stdout:setvbuf 'no'
+ pub.task('loadProto')
+end
+
+return m
diff --git a/script-beta/src/provider/capability.lua b/script-beta/src/provider/capability.lua
new file mode 100644
index 00000000..aa95c758
--- /dev/null
+++ b/script-beta/src/provider/capability.lua
@@ -0,0 +1,42 @@
+local m = {}
+
+m.initer = {
+ -- 文本同步方式
+ textDocumentSync = {
+ -- 打开关闭文本时通知
+ openClose = true,
+ -- 文本改变时完全通知 TODO 支持差量更新(2)
+ change = 1,
+ },
+
+ hoverProvider = true,
+ definitionProvider = true,
+ referencesProvider = true,
+ renameProvider = {
+ prepareProvider = true,
+ },
+ --documentSymbolProvider = true,
+ documentHighlightProvider = true,
+ --codeActionProvider = true,
+ --signatureHelpProvider = {
+ -- triggerCharacters = { '(', ',' },
+ --},
+ --workspace = {
+ -- workspaceFolders = {
+ -- supported = true,
+ -- changeNotifications = true,
+ -- }
+ --},
+ --documentOnTypeFormattingProvider = {
+ -- firstTriggerCharacter = '}',
+ --},
+ --executeCommandProvider = {
+ -- commands = {
+ -- 'config',
+ -- 'removeSpace',
+ -- 'solve',
+ -- },
+ --},
+}
+
+return m
diff --git a/script-beta/src/provider/completion.lua b/script-beta/src/provider/completion.lua
new file mode 100644
index 00000000..d2df44d2
--- /dev/null
+++ b/script-beta/src/provider/completion.lua
@@ -0,0 +1,53 @@
+local proto = require 'proto'
+
+local isEnable = false
+
+local function allWords()
+ local str = [[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:('"[,#*@| ]]
+ local list = {}
+ for c in str:gmatch '.' do
+ list[#list+1] = c
+ end
+ return list
+end
+
+local function enable()
+ if isEnable then
+ return
+ end
+ isEnable = true
+ log.debug('Enable completion.')
+ proto.awaitRequest('client/registerCapability', {
+ registrations = {
+ {
+ id = 'completion',
+ method = 'textDocument/completion',
+ registerOptions = {
+ resolveProvider = false,
+ triggerCharacters = allWords(),
+ },
+ },
+ }
+ })
+end
+
+local function disable()
+ if not isEnable then
+ return
+ end
+ isEnable = false
+ log.debug('Disable completion.')
+ proto.awaitRequest('client/unregisterCapability', {
+ unregisterations = {
+ {
+ id = 'completion',
+ method = 'textDocument/completion',
+ },
+ }
+ })
+end
+
+return {
+ enable = enable,
+ disable = disable,
+}
diff --git a/script-beta/src/provider/diagnostic.lua b/script-beta/src/provider/diagnostic.lua
new file mode 100644
index 00000000..ba95f2bf
--- /dev/null
+++ b/script-beta/src/provider/diagnostic.lua
@@ -0,0 +1,209 @@
+local await = require 'await'
+local proto = require 'proto.proto'
+local define = require 'proto.define'
+local lang = require 'language'
+local files = require 'files'
+local config = require 'config'
+local core = require 'core.diagnostics'
+local util = require 'utility'
+
+local m = {}
+m._start = false
+m.cache = {}
+
+local function concat(t, sep)
+ if type(t) ~= 'table' then
+ return t
+ end
+ return table.concat(t, sep)
+end
+
+local function buildSyntaxError(uri, err)
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local message = lang.script('PARSER_'..err.type, err.info)
+
+ if err.version then
+ local version = err.info and err.info.version or config.config.runtime.version
+ message = message .. ('(%s)'):format(lang.script('DIAG_NEED_VERSION'
+ , concat(err.version, '/')
+ , version
+ ))
+ end
+
+ local related = err.info and err.info.related
+ local relatedInformation
+ if related then
+ relatedInformation = {}
+ for _, rel in ipairs(related) do
+ local rmessage
+ if rel.message then
+ rmessage = lang.script('PARSER_'..rel.message)
+ else
+ rmessage = text:sub(rel.start, rel.finish)
+ end
+ relatedInformation[#relatedInformation+1] = {
+ message = rmessage,
+ location = define.location(uri, define.range(lines, text, rel.start, rel.finish)),
+ }
+ end
+ end
+
+ return {
+ range = define.range(lines, text, err.start, err.finish),
+ severity = define.DiagnosticSeverity.Error,
+ source = lang.script.DIAG_SYNTAX_CHECK,
+ message = message,
+ relatedInformation = relatedInformation,
+ }
+end
+
+local function buildDiagnostic(uri, diag)
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+
+ local relatedInformation
+ if diag.related then
+ relatedInformation = {}
+ for _, rel in ipairs(diag.related) do
+ local rtext = files.getText(rel.uri)
+ local rlines = files.getLines(rel.uri)
+ relatedInformation[#relatedInformation+1] = {
+ message = rel.message or rtext:sub(rel.start, rel.finish),
+ location = define.location(rel.uri, define.range(rlines, rtext, rel.start, rel.finish))
+ }
+ end
+ end
+
+ return {
+ range = define.range(lines, text, diag.start, diag.finish),
+ source = lang.script.DIAG_DIAGNOSTICS,
+ severity = diag.level,
+ message = diag.message,
+ code = diag.code,
+ tags = diag.tags,
+ relatedInformation = relatedInformation,
+ }
+end
+
+local function merge(a, b)
+ if not a and not b then
+ return nil
+ end
+ local t = {}
+ if a then
+ for i = 1, #a do
+ t[#t+1] = a[i]
+ end
+ end
+ if b then
+ for i = 1, #b do
+ t[#t+1] = b[i]
+ end
+ end
+ return t
+end
+
+function m.clear(uri)
+ if not m.cache[uri] then
+ return
+ end
+ m.cache[uri] = nil
+ proto.notify('textDocument/publishDiagnostics', {
+ uri = uri,
+ diagnostics = {},
+ })
+end
+
+function m.syntaxErrors(uri, ast)
+ if #ast.errs == 0 then
+ return nil
+ end
+
+ local results = {}
+
+ for _, err in ipairs(ast.errs) do
+ results[#results+1] = buildSyntaxError(uri, err)
+ end
+
+ return results
+end
+
+function m.diagnostics(uri, syntaxOnly)
+ if syntaxOnly or not m._start then
+ return m.cache[uri]
+ end
+
+ local diags = core(uri)
+ if not diags then
+ return nil
+ end
+
+ local results = {}
+ for _, diag in ipairs(diags) do
+ results[#results+1] = buildDiagnostic(uri, diag)
+ end
+
+ return results
+end
+
+function m.doDiagnostic(uri, syntaxOnly)
+ local ast = files.getAst(uri)
+ if not ast then
+ m.clear(uri)
+ return
+ end
+
+ local syntax = m.syntaxErrors(uri, ast)
+ local diagnostics = m.diagnostics(uri, syntaxOnly)
+ local full = merge(syntax, diagnostics)
+ if not full then
+ m.clear(uri)
+ return
+ end
+
+ if util.equal(m.cache[uri], full) then
+ return
+ end
+ m.cache[uri] = full
+
+ proto.notify('textDocument/publishDiagnostics', {
+ uri = uri,
+ diagnostics = full,
+ })
+end
+
+function m.refresh(uri)
+ await.create(function ()
+ await.delay(function ()
+ return files.globalVersion
+ end)
+ if uri then
+ m.doDiagnostic(uri, true)
+ end
+ if not m._start then
+ return
+ end
+ local clock = os.clock()
+ if uri then
+ m.doDiagnostic(uri)
+ end
+ for destUri in files.eachFile() do
+ if destUri ~= uri then
+ m.doDiagnostic(files.getOriginUri(destUri))
+ await.delay(function ()
+ return files.globalVersion
+ end)
+ end
+ end
+ local passed = os.clock() - clock
+ log.info(('Finish diagnostics, takes [%.3f] sec.'):format(passed))
+ end)
+end
+
+function m.start()
+ m._start = true
+ m.refresh()
+end
+
+return m
diff --git a/script-beta/src/provider/init.lua b/script-beta/src/provider/init.lua
new file mode 100644
index 00000000..95f4b3d1
--- /dev/null
+++ b/script-beta/src/provider/init.lua
@@ -0,0 +1,298 @@
+local util = require 'utility'
+local cap = require 'provider.capability'
+local completion= require 'provider.completion'
+local await = require 'await'
+local files = require 'files'
+local proto = require 'proto.proto'
+local define = require 'proto.define'
+local workspace = require 'workspace'
+local config = require 'config'
+local library = require 'library'
+local markdown = require 'provider.markdown'
+
+local function updateConfig()
+ local configs = proto.awaitRequest('workspace/configuration', {
+ items = {
+ {
+ scopeUri = workspace.uri,
+ section = 'Lua',
+ },
+ {
+ scopeUri = workspace.uri,
+ section = 'files.associations',
+ },
+ {
+ scopeUri = workspace.uri,
+ section = 'files.exclude',
+ }
+ },
+ })
+
+ local updated = configs[1]
+ local other = {
+ associations = configs[2],
+ exclude = configs[3],
+ }
+
+ local oldConfig = util.deepCopy(config.config)
+ local oldOther = util.deepCopy(config.other)
+ config.setConfig(updated, other)
+ local newConfig = config.config
+ local newOther = config.other
+ if not util.equal(oldConfig.runtime, newConfig.runtime) then
+ library.reload()
+ end
+ if not util.equal(oldConfig.diagnostics, newConfig.diagnostics) then
+ end
+ if not util.equal(oldConfig.plugin, newConfig.plugin) then
+ end
+ if not util.equal(oldConfig.workspace, newConfig.workspace)
+ or not util.equal(oldConfig.plugin, newConfig.plugin)
+ or not util.equal(oldOther.associations, newOther.associations)
+ or not util.equal(oldOther.exclude, newOther.exclude)
+ then
+ end
+
+ if newConfig.completion.enable then
+ --completion.enable()
+ else
+ completion.disable()
+ end
+end
+
+proto.on('initialize', function (params)
+ --log.debug(util.dump(params))
+ if params.workspaceFolders then
+ local name = params.workspaceFolders[1].name
+ local uri = params.workspaceFolders[1].uri
+ workspace.init(name, uri)
+ end
+ return {
+ capabilities = cap.initer,
+ }
+end)
+
+proto.on('initialized', function (params)
+ updateConfig()
+ proto.awaitRequest('client/registerCapability', {
+ registrations = {
+ -- 监视文件变化
+ {
+ id = '0',
+ method = 'workspace/didChangeWatchedFiles',
+ registerOptions = {
+ watchers = {
+ {
+ globPattern = '**/',
+ kind = 1 | 2 | 4,
+ }
+ },
+ },
+ },
+ -- 配置变化
+ {
+ id = '1',
+ method = 'workspace/didChangeConfiguration',
+ }
+ }
+ })
+ await.create(workspace.awaitPreload)
+ return true
+end)
+
+proto.on('exit', function ()
+ log.info('Server exited.')
+ os.exit(true)
+end)
+
+proto.on('shutdown', function ()
+ log.info('Server shutdown.')
+ return true
+end)
+
+proto.on('workspace/configuration', function ()
+ updateConfig()
+end)
+
+proto.on('workspace/didChangeWatchedFiles', function (params)
+end)
+
+proto.on('textDocument/didOpen', function (params)
+ local doc = params.textDocument
+ local uri = doc.uri
+ local text = doc.text
+ files.open(uri)
+ files.setText(uri, text)
+end)
+
+proto.on('textDocument/didClose', function (params)
+ local doc = params.textDocument
+ local uri = doc.uri
+ files.close(uri)
+ if not files.isLua(uri) then
+ files.remove(uri)
+ end
+end)
+
+proto.on('textDocument/didChange', function (params)
+ local doc = params.textDocument
+ local change = params.contentChanges
+ local uri = doc.uri
+ local text = change[1].text
+ if files.isLua(uri) or files.isOpen(uri) then
+ files.setText(uri, text)
+ end
+end)
+
+proto.on('textDocument/hover', function (params)
+ local core = require 'core.hover'
+ local doc = params.textDocument
+ local uri = doc.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local hover = core(uri, offset)
+ if not hover then
+ return nil
+ end
+ local md = markdown()
+ md:add('lua', hover.label)
+ return {
+ contents = {
+ value = md:string(),
+ kind = 'markdown',
+ },
+ range = define.range(lines, text, hover.source.start, hover.source.finish),
+ }
+end)
+
+proto.on('textDocument/definition', function (params)
+ local core = require 'core.definition'
+ local uri = params.textDocument.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local result = core(uri, offset)
+ if not result then
+ return nil
+ end
+ local response = {}
+ for i, info in ipairs(result) do
+ local targetUri = info.uri
+ local targetLines = files.getLines(targetUri)
+ local targetText = files.getText(targetUri)
+ response[i] = define.locationLink(targetUri
+ , define.range(targetLines, targetText, info.target.start, info.target.finish)
+ , define.range(targetLines, targetText, info.target.start, info.target.finish)
+ , define.range(lines, text, info.source.start, info.source.finish)
+ )
+ end
+ return response
+end)
+
+proto.on('textDocument/references', function (params)
+ local core = require 'core.reference'
+ local uri = params.textDocument.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local result = core(uri, offset)
+ if not result then
+ return nil
+ end
+ local response = {}
+ for i, info in ipairs(result) do
+ local targetUri = info.uri
+ local targetLines = files.getLines(targetUri)
+ local targetText = files.getText(targetUri)
+ response[i] = define.location(targetUri
+ , define.range(targetLines, targetText, info.target.start, info.target.finish)
+ )
+ end
+ return response
+end)
+
+proto.on('textDocument/documentHighlight', function (params)
+ local core = require 'core.highlight'
+ local uri = params.textDocument.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local result = core(uri, offset)
+ if not result then
+ return nil
+ end
+ local response = {}
+ for _, info in ipairs(result) do
+ response[#response+1] = {
+ range = define.range(lines, text, info.start, info.finish),
+ kind = info.kind,
+ }
+ end
+ return response
+end)
+
+proto.on('textDocument/rename', function (params)
+ local core = require 'core.rename'
+ local uri = params.textDocument.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local result = core.rename(uri, offset, params.newName)
+ if not result then
+ return nil
+ end
+ local workspaceEdit = {
+ changes = {},
+ }
+ for _, info in ipairs(result) do
+ local ruri = info.uri
+ local rlines = files.getLines(ruri)
+ local rtext = files.getText(ruri)
+ if not workspaceEdit.changes[ruri] then
+ workspaceEdit.changes[ruri] = {}
+ end
+ local textEdit = define.textEdit(define.range(rlines, rtext, info.start, info.finish), info.text)
+ workspaceEdit.changes[ruri][#workspaceEdit.changes[ruri]+1] = textEdit
+ end
+ return workspaceEdit
+end)
+
+proto.on('textDocument/prepareRename', function (params)
+ local core = require 'core.rename'
+ local uri = params.textDocument.uri
+ if not files.exists(uri) then
+ return nil
+ end
+ local lines = files.getLines(uri)
+ local text = files.getText(uri)
+ local offset = define.offset(lines, text, params.position)
+ local result = core.prepareRename(uri, offset)
+ if not result then
+ return nil
+ end
+ return {
+ range = define.range(lines, text, result.start, result.finish),
+ placeholder = result.text,
+ }
+end)
+
+proto.on('textDocument/completion', function (params)
+ --log.info(util.dump(params))
+ return nil
+end)
diff --git a/script-beta/src/provider/markdown.lua b/script-beta/src/provider/markdown.lua
new file mode 100644
index 00000000..0f69ad87
--- /dev/null
+++ b/script-beta/src/provider/markdown.lua
@@ -0,0 +1,22 @@
+local mt = {}
+mt.__index = mt
+mt.__name = 'markdown'
+
+function mt:add(language, text)
+ if not text then
+ return
+ end
+ if language == 'lua' then
+ self[#self+1] = ('```lua\n%s\n```'):format(text)
+ else
+ self[#self+1] = text:gsub('\n', '\n\n')
+ end
+end
+
+function mt:string()
+ return table.concat(self, '\n')
+end
+
+return function ()
+ return setmetatable({}, mt)
+end
diff --git a/script-beta/src/pub/init.lua b/script-beta/src/pub/init.lua
new file mode 100644
index 00000000..61b43da7
--- /dev/null
+++ b/script-beta/src/pub/init.lua
@@ -0,0 +1,4 @@
+local pub = require 'pub.pub'
+require 'pub.report'
+
+return pub
diff --git a/script-beta/src/pub/pub.lua b/script-beta/src/pub/pub.lua
new file mode 100644
index 00000000..2cb1b4e8
--- /dev/null
+++ b/script-beta/src/pub/pub.lua
@@ -0,0 +1,236 @@
+local thread = require 'bee.thread'
+local utility = require 'utility'
+local await = require 'await'
+local timer = require 'timer'
+
+local errLog = thread.channel 'errlog'
+local type = type
+local counter = utility.counter()
+
+local braveTemplate = [[
+package.path = %q
+package.cpath = %q
+
+collectgarbage 'generational'
+
+log = require 'brave.log'
+
+dofile(%q)
+local brave = require 'brave'
+brave.register(%d)
+]]
+
+---@class pub
+local m = {}
+m.type = 'pub'
+m.braves = {}
+m.ability = {}
+m.taskQueue = {}
+
+--- 注册酒馆的功能
+function m.on(name, callback)
+ m.ability[name] = callback
+end
+
+--- 招募勇者,勇者会从公告板上领取任务,完成任务后到看板娘处交付任务
+---@param num integer
+function m.recruitBraves(num)
+ for _ = 1, num do
+ local id = #m.braves + 1
+ log.info('Create brave:', id)
+ thread.newchannel('taskpad' .. id)
+ thread.newchannel('waiter' .. id)
+ m.braves[id] = {
+ id = id,
+ taskpad = thread.channel('taskpad' .. id),
+ waiter = thread.channel('waiter' .. id),
+ thread = thread.thread(braveTemplate:format(
+ package.path,
+ package.cpath,
+ (ROOT / 'debugger.lua'):string(),
+ id
+ )),
+ taskMap = {},
+ currentTask = nil,
+ memory = 0,
+ }
+ end
+end
+
+--- 勇者是否有空
+function m.isIdle(brave)
+ return next(brave.taskMap) == nil
+end
+
+--- 给勇者推送任务
+function m.pushTask(brave, info)
+ if info.removed then
+ return false
+ end
+ brave.taskpad:push(info.name, info.id, info.params)
+ brave.taskMap[info.id] = info
+ --log.info(('Push task %q(%d) to # %d, queue length %d'):format(info.name, info.id, brave.id, #m.taskQueue))
+ return true
+end
+
+--- 从勇者处接收任务反馈
+function m.popTask(brave, id, result)
+ local info = brave.taskMap[id]
+ if not info then
+ log.warn(('Brave pushed unknown task result: # %d => [%d]'):format(brave.id, id))
+ return
+ end
+ brave.taskMap[id] = nil
+ --log.info(('Pop task %q(%d) from # %d'):format(info.name, info.id, brave.id))
+ m.checkWaitingTask(brave)
+ if not info.removed then
+ info.removed = true
+ if info.callback then
+ xpcall(info.callback, log.error, result)
+ end
+ end
+end
+
+--- 从勇者处接收报告
+function m.popReport(brave, name, params)
+ local abil = m.ability[name]
+ if not abil then
+ log.warn(('Brave pushed unknown report: # %d => %q'):format(brave.id, name))
+ return
+ end
+ xpcall(abil, log.error, params, brave)
+end
+
+--- 发布任务
+---@parma name string
+---@param params any
+function m.awaitTask(name, params)
+ local info = {
+ id = counter(),
+ name = name,
+ params = params,
+ }
+ for _, brave in ipairs(m.braves) do
+ if m.isIdle(brave) then
+ if m.pushTask(brave, info) then
+ return await.wait(function (waker)
+ info.callback = waker
+ end)
+ else
+ return nil
+ end
+ end
+ end
+ -- 如果所有勇者都在战斗,那么把任务缓存到队列里
+ -- 当有勇者提交任务反馈后,尝试把按顺序将堆积任务
+ -- 交给该勇者
+ m.taskQueue[#m.taskQueue+1] = info
+ --log.info(('Add task %q(%d) in queue, length %d.'):format(name, info.id, #m.taskQueue))
+ return await.wait(function (waker)
+ info.callback = waker
+ end)
+end
+
+--- 发布同步任务,如果任务进入了队列,会返回执行器
+--- 通过 jumpQueue 可以插队
+---@parma name string
+---@param params any
+---@param callback function
+function m.task(name, params, callback)
+ local info = {
+ id = counter(),
+ name = name,
+ params = params,
+ callback = callback,
+ }
+ for _, brave in ipairs(m.braves) do
+ if m.isIdle(brave) then
+ m.pushTask(brave, info)
+ return nil
+ end
+ end
+ -- 如果所有勇者都在战斗,那么把任务缓存到队列里
+ -- 当有勇者提交任务反馈后,尝试把按顺序将堆积任务
+ -- 交给该勇者
+ m.taskQueue[#m.taskQueue+1] = info
+ --log.info(('Add task %q(%d) in queue, length %d.'):format(name, info.id, #m.taskQueue))
+ return info
+end
+
+--- 插队
+function m.jumpQueue(info)
+ for i = 2, #m.taskQueue do
+ if m.taskQueue[i] == info then
+ m.taskQueue[i] = nil
+ table.move(m.taskQueue, 1, i - 1, 2)
+ m.taskQueue[1] = info
+ return
+ end
+ end
+end
+
+--- 移除任务
+function m.remove(info)
+ info.removed = true
+ for i = 1, #m.taskQueue do
+ if m.taskQueue[i] == info then
+ table.remove(m.taskQueue[i], i)
+ return
+ end
+ end
+end
+
+--- 检查堆积任务
+function m.checkWaitingTask(brave)
+ if #m.taskQueue == 0 then
+ return
+ end
+ -- 如果勇者还有其他活要忙,那么让他继续忙去吧
+ if next(brave.taskMap) then
+ return
+ end
+ while #m.taskQueue > 0 do
+ local info = table.remove(m.taskQueue, 1)
+ if m.pushTask(brave, info) then
+ break
+ end
+ end
+end
+
+--- 接收反馈
+---|返回接收到的反馈数量
+---@return integer
+function m.recieve()
+ for _, brave in ipairs(m.braves) do
+ while true do
+ local suc, id, result = brave.waiter:pop()
+ if not suc then
+ goto CONTINUE
+ end
+ if type(id) == 'string' then
+ m.popReport(brave, id, result)
+ else
+ m.popTask(brave, id, result)
+ end
+ end
+ ::CONTINUE::
+ end
+end
+
+--- 检查伤亡情况
+function m.checkDead()
+ while true do
+ local suc, err = errLog:pop()
+ if not suc then
+ break
+ end
+ log.error('Brave is dead!: ' .. err)
+ end
+end
+
+function m.step()
+ m.checkDead()
+ m.recieve()
+end
+
+return m
diff --git a/script-beta/src/pub/report.lua b/script-beta/src/pub/report.lua
new file mode 100644
index 00000000..edd3ee0e
--- /dev/null
+++ b/script-beta/src/pub/report.lua
@@ -0,0 +1,21 @@
+local pub = require 'pub.pub'
+local await = require 'await'
+
+pub.on('log', function (params, brave)
+ log.raw(brave.id, params.level, params.msg, params.src, params.line)
+end)
+
+pub.on('mem', function (count, brave)
+ brave.memory = count
+end)
+
+pub.on('proto', function (params)
+ local proto = require 'proto'
+ await.create(function ()
+ if params.method then
+ proto.doMethod(params)
+ else
+ proto.doResponse(params)
+ end
+ end)
+end)
diff --git a/script-beta/src/service/init.lua b/script-beta/src/service/init.lua
new file mode 100644
index 00000000..eb0bd057
--- /dev/null
+++ b/script-beta/src/service/init.lua
@@ -0,0 +1,3 @@
+local service = require 'service.service'
+
+return service
diff --git a/script-beta/src/service/service.lua b/script-beta/src/service/service.lua
new file mode 100644
index 00000000..e1cb604b
--- /dev/null
+++ b/script-beta/src/service/service.lua
@@ -0,0 +1,137 @@
+local pub = require 'pub'
+local thread = require 'bee.thread'
+local await = require 'await'
+local timer = require 'timer'
+local proto = require 'proto'
+local vm = require 'vm'
+
+local m = {}
+m.type = 'service'
+
+local function countMemory()
+ local mems = {}
+ local total = 0
+ mems[0] = collectgarbage 'count'
+ total = total + collectgarbage 'count'
+ for id, brave in ipairs(pub.braves) do
+ mems[id] = brave.memory
+ total = total + brave.memory
+ end
+ return total, mems
+end
+
+function m.reportMemoryCollect()
+ local totalMemBefore = countMemory()
+ local clock = os.clock()
+ collectgarbage()
+ local passed = os.clock() - clock
+ local totalMemAfter, mems = countMemory()
+
+ local lines = {}
+ lines[#lines+1] = ' --------------- Memory ---------------'
+ lines[#lines+1] = (' Total: %.3f(%.3f) MB'):format(totalMemAfter / 1000.0, totalMemBefore / 1000.0)
+ for i = 0, #mems do
+ lines[#lines+1] = (' # %02d : %.3f MB'):format(i, mems[i] / 1000.0)
+ end
+ lines[#lines+1] = (' Collect garbage takes [%.3f] sec'):format(passed)
+ return table.concat(lines, '\n')
+end
+
+function m.reportMemory()
+ local totalMem, mems = countMemory()
+
+ local lines = {}
+ lines[#lines+1] = ' --------------- Memory ---------------'
+ lines[#lines+1] = (' Total: %.3f MB'):format(totalMem / 1000.0)
+ for i = 0, #mems do
+ lines[#lines+1] = (' # %02d : %.3f MB'):format(i, mems[i] / 1000.0)
+ end
+ return table.concat(lines, '\n')
+end
+
+function m.reportTask()
+ local total = 0
+ local running = 0
+ local suspended = 0
+ local normal = 0
+ local dead = 0
+
+ for co in pairs(await.coTracker) do
+ total = total + 1
+ local status = coroutine.status(co)
+ if status == 'running' then
+ running = running + 1
+ elseif status == 'suspended' then
+ suspended = suspended + 1
+ elseif status == 'normal' then
+ normal = normal + 1
+ elseif status == 'dead' then
+ dead = dead + 1
+ end
+ end
+
+ local lines = {}
+ lines[#lines+1] = ' --------------- Coroutine ---------------'
+ lines[#lines+1] = (' Total: %d'):format(total)
+ lines[#lines+1] = (' Running: %d'):format(running)
+ lines[#lines+1] = (' Suspended: %d'):format(suspended)
+ lines[#lines+1] = (' Normal: %d'):format(normal)
+ lines[#lines+1] = (' Dead: %d'):format(dead)
+ return table.concat(lines, '\n')
+end
+
+function m.reportCache()
+ local total = 0
+ local dead = 0
+
+ for cache in pairs(vm.cacheTracker) do
+ total = total + 1
+ if cache.dead then
+ dead = dead + 1
+ end
+ end
+
+ local lines = {}
+ lines[#lines+1] = ' --------------- Cache ---------------'
+ lines[#lines+1] = (' Total: %d'):format(total)
+ lines[#lines+1] = (' Dead: %d'):format(dead)
+ return table.concat(lines, '\n')
+end
+
+function m.report()
+ local t = timer.loop(60.0, function ()
+ local lines = {}
+ lines[#lines+1] = ''
+ lines[#lines+1] = '========= Medical Examination Report ========='
+ lines[#lines+1] = m.reportMemory()
+ lines[#lines+1] = m.reportTask()
+ lines[#lines+1] = m.reportCache()
+ lines[#lines+1] = '=============================================='
+
+ log.debug(table.concat(lines, '\n'))
+ end)
+ t:onTimer()
+end
+
+function m.startTimer()
+ while true do
+ pub.step()
+ if not await.step() then
+ thread.sleep(0.001)
+ timer.update()
+ end
+ end
+end
+
+function m.start()
+ await.setErrorHandle(log.error)
+ pub.recruitBraves(4)
+ proto.listen()
+ m.report()
+
+ require 'provider'
+
+ m.startTimer()
+end
+
+return m
diff --git a/script-beta/src/timer.lua b/script-beta/src/timer.lua
new file mode 100644
index 00000000..1d4343f1
--- /dev/null
+++ b/script-beta/src/timer.lua
@@ -0,0 +1,218 @@
+local setmetatable = setmetatable
+local mathMax = math.max
+local mathFloor = math.floor
+local osClock = os.clock
+
+_ENV = nil
+
+local curFrame = 0
+local maxFrame = 0
+local curIndex = 0
+local freeQueue = {}
+local timer = {}
+
+local function allocQueue()
+ local n = #freeQueue
+ if n > 0 then
+ local r = freeQueue[n]
+ freeQueue[n] = nil
+ return r
+ else
+ return {}
+ end
+end
+
+local function mTimeout(self, timeout)
+ if self._pauseRemaining or self._running then
+ return
+ end
+ local ti = curFrame + timeout
+ local q = timer[ti]
+ if q == nil then
+ q = allocQueue()
+ timer[ti] = q
+ end
+ self._timeoutFrame = ti
+ self._running = true
+ q[#q + 1] = self
+end
+
+local function mWakeup(self)
+ if self._removed then
+ return
+ end
+ self._running = false
+ if self._onTimer then
+ self:_onTimer()
+ end
+ if self._removed then
+ return
+ end
+ if self._timerCount then
+ if self._timerCount > 1 then
+ self._timerCount = self._timerCount - 1
+ mTimeout(self, self._timeout)
+ else
+ self._removed = true
+ end
+ else
+ mTimeout(self, self._timeout)
+ end
+end
+
+local function getRemaining(self)
+ if self._removed then
+ return 0
+ end
+ if self._pauseRemaining then
+ return self._pauseRemaining
+ end
+ if self._timeoutFrame == curFrame then
+ return self._timeout or 0
+ end
+ return self._timeoutFrame - curFrame
+end
+
+local function onTick()
+ local q = timer[curFrame]
+ if q == nil then
+ curIndex = 0
+ return
+ end
+ for i = curIndex + 1, #q do
+ local callback = q[i]
+ curIndex = i
+ q[i] = nil
+ if callback then
+ mWakeup(callback)
+ end
+ end
+ curIndex = 0
+ timer[curFrame] = nil
+ freeQueue[#freeQueue + 1] = q
+end
+
+local m = {}
+local mt = {}
+mt.__index = mt
+mt.type = 'timer'
+
+function mt:__tostring()
+ return '[table:timer]'
+end
+
+function mt:__call()
+ if self._onTimer then
+ self:_onTimer()
+ end
+end
+
+function mt:remove()
+ self._removed = true
+end
+
+function mt:pause()
+ if self._removed or self._pauseRemaining then
+ return
+ end
+ self._pauseRemaining = getRemaining(self)
+ self._running = false
+ local ti = self._timeoutFrame
+ local q = timer[ti]
+ if q then
+ for i = #q, 1, -1 do
+ if q[i] == self then
+ q[i] = false
+ return
+ end
+ end
+ end
+end
+
+function mt:resume()
+ if self._removed or not self._pauseRemaining then
+ return
+ end
+ local timeout = self._pauseRemaining
+ self._pauseRemaining = nil
+ mTimeout(self, timeout)
+end
+
+function mt:restart()
+ if self._removed or self._pauseRemaining or not self._running then
+ return
+ end
+ local ti = self._timeoutFrame
+ local q = timer[ti]
+ if q then
+ for i = #q, 1, -1 do
+ if q[i] == self then
+ q[i] = false
+ break
+ end
+ end
+ end
+ self._running = false
+ mTimeout(self, self._timeout)
+end
+
+function mt:remaining()
+ return getRemaining(self) / 1000.0
+end
+
+function mt:onTimer()
+ self:_onTimer()
+end
+
+function m.wait(timeout, onTimer)
+ local t = setmetatable({
+ ['_timeout'] = mathMax(mathFloor(timeout * 1000.0), 1),
+ ['_onTimer'] = onTimer,
+ ['_timerCount'] = 1,
+ }, mt)
+ mTimeout(t, t._timeout)
+ return t
+end
+
+function m.loop(timeout, onTimer)
+ local t = setmetatable({
+ ['_timeout'] = mathFloor(timeout * 1000.0),
+ ['_onTimer'] = onTimer,
+ }, mt)
+ mTimeout(t, t._timeout)
+ return t
+end
+
+function m.timer(timeout, count, onTimer)
+ if count == 0 then
+ return m.loop(timeout, onTimer)
+ end
+ local t = setmetatable({
+ ['_timeout'] = mathFloor(timeout * 1000.0),
+ ['_onTimer'] = onTimer,
+ ['_timerCount'] = count,
+ }, mt)
+ mTimeout(t, t._timeout)
+ return t
+end
+
+function m.clock()
+ return curFrame / 1000.0
+end
+
+local lastClock = osClock()
+function m.update()
+ local currentClock = osClock()
+ local delta = currentClock - lastClock
+ lastClock = currentClock
+ if curIndex ~= 0 then
+ curFrame = curFrame - 1
+ end
+ maxFrame = maxFrame + delta * 1000.0
+ while curFrame < maxFrame do
+ curFrame = curFrame + 1
+ onTick()
+ end
+end
+
+return m
diff --git a/script-beta/src/utility.lua b/script-beta/src/utility.lua
new file mode 100644
index 00000000..c9defebc
--- /dev/null
+++ b/script-beta/src/utility.lua
@@ -0,0 +1,452 @@
+local tableSort = table.sort
+local stringRep = string.rep
+local tableConcat = table.concat
+local tostring = tostring
+local type = type
+local pairs = pairs
+local ipairs = ipairs
+local next = next
+local rawset = rawset
+local move = table.move
+local setmetatable = setmetatable
+local mathType = math.type
+local mathCeil = math.ceil
+local getmetatable = getmetatable
+local mathAbs = math.abs
+local ioOpen = io.open
+
+_ENV = nil
+
+local function formatNumber(n)
+ local str = ('%.10f'):format(n)
+ str = str:gsub('%.?0*$', '')
+ return str
+end
+
+local function isInteger(n)
+ if mathType then
+ return mathType(n) == 'integer'
+ else
+ return type(n) == 'number' and n % 1 == 0
+ end
+end
+
+local TAB = setmetatable({}, { __index = function (self, n)
+ self[n] = stringRep(' ', n)
+ return self[n]
+end})
+
+local RESERVED = {
+ ['and'] = true,
+ ['break'] = true,
+ ['do'] = true,
+ ['else'] = true,
+ ['elseif'] = true,
+ ['end'] = true,
+ ['false'] = true,
+ ['for'] = true,
+ ['function'] = true,
+ ['goto'] = 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,
+}
+
+local m = {}
+
+--- 打印表的结构
+---@param tbl table
+---@param option table {optional = 'self'}
+---@return string
+function m.dump(tbl, option)
+ if not option then
+ option = {}
+ end
+ if type(tbl) ~= 'table' then
+ return ('%s'):format(tbl)
+ end
+ local lines = {}
+ local mark = {}
+ lines[#lines+1] = '{'
+ local function unpack(tbl, tab)
+ mark[tbl] = (mark[tbl] or 0) + 1
+ local keys = {}
+ local keymap = {}
+ local integerFormat = '[%d]'
+ local alignment = 0
+ if #tbl >= 10 then
+ local width = #tostring(#tbl)
+ integerFormat = ('[%%0%dd]'):format(mathCeil(width))
+ end
+ for key in pairs(tbl) do
+ if type(key) == 'string' then
+ if not key:match('^[%a_][%w_]*$')
+ or RESERVED[key]
+ or option['longStringKey']
+ then
+ keymap[key] = ('[%q]'):format(key)
+ else
+ keymap[key] = ('%s'):format(key)
+ end
+ elseif isInteger(key) then
+ keymap[key] = integerFormat:format(key)
+ else
+ keymap[key] = ('["<%s>"]'):format(tostring(key))
+ end
+ keys[#keys+1] = key
+ if option['alignment'] then
+ if #keymap[key] > alignment then
+ alignment = #keymap[key]
+ end
+ end
+ end
+ local mt = getmetatable(tbl)
+ if not mt or not mt.__pairs then
+ if option['sorter'] then
+ option['sorter'](keys, keymap)
+ else
+ tableSort(keys, function (a, b)
+ return keymap[a] < keymap[b]
+ end)
+ end
+ end
+ for _, key in ipairs(keys) do
+ local keyWord = keymap[key]
+ if option['noArrayKey']
+ and isInteger(key)
+ and key <= #tbl
+ then
+ keyWord = ''
+ else
+ if #keyWord < alignment then
+ keyWord = keyWord .. (' '):rep(alignment - #keyWord) .. ' = '
+ else
+ keyWord = keyWord .. ' = '
+ end
+ end
+ local value = tbl[key]
+ local tp = type(value)
+ if option['format'] and option['format'][key] then
+ lines[#lines+1] = ('%s%s%s,'):format(TAB[tab+1], keyWord, option['format'][key](value, unpack, tab+1))
+ elseif tp == 'table' then
+ if mark[value] and mark[value] > 0 then
+ lines[#lines+1] = ('%s%s%s,'):format(TAB[tab+1], keyWord, option['loop'] or '"<Loop>"')
+ else
+ lines[#lines+1] = ('%s%s{'):format(TAB[tab+1], keyWord)
+ unpack(value, tab+1)
+ lines[#lines+1] = ('%s},'):format(TAB[tab+1])
+ end
+ elseif tp == 'string' then
+ lines[#lines+1] = ('%s%s%q,'):format(TAB[tab+1], keyWord, value)
+ elseif tp == 'number' then
+ lines[#lines+1] = ('%s%s%s,'):format(TAB[tab+1], keyWord, (option['number'] or formatNumber)(value))
+ elseif tp == 'nil' then
+ else
+ lines[#lines+1] = ('%s%s%s,'):format(TAB[tab+1], keyWord, tostring(value))
+ end
+ end
+ mark[tbl] = mark[tbl] - 1
+ end
+ unpack(tbl, 0)
+ lines[#lines+1] = '}'
+ return tableConcat(lines, '\r\n')
+end
+
+--- 递归判断A与B是否相等
+---@param a any
+---@param b any
+---@return boolean
+function m.equal(a, b)
+ local tp1 = type(a)
+ local tp2 = type(b)
+ if tp1 ~= tp2 then
+ return false
+ end
+ if tp1 == 'table' then
+ local mark = {}
+ for k, v in pairs(a) do
+ mark[k] = true
+ local res = m.equal(v, b[k])
+ if not res then
+ return false
+ end
+ end
+ for k in pairs(b) do
+ if not mark[k] then
+ return false
+ end
+ end
+ return true
+ elseif tp1 == 'number' then
+ return mathAbs(a - b) <= 1e-10
+ else
+ return a == b
+ end
+end
+
+local function sortTable(tbl)
+ if not tbl then
+ tbl = {}
+ end
+ local mt = {}
+ local keys = {}
+ local mark = {}
+ local n = 0
+ for key in next, tbl do
+ n=n+1;keys[n] = key
+ mark[key] = true
+ end
+ tableSort(keys)
+ function mt:__newindex(key, value)
+ rawset(self, key, value)
+ n=n+1;keys[n] = key
+ mark[key] = true
+ if type(value) == 'table' then
+ sortTable(value)
+ end
+ end
+ function mt:__pairs()
+ local list = {}
+ local m = 0
+ for key in next, self do
+ if not mark[key] then
+ m=m+1;list[m] = key
+ end
+ end
+ if m > 0 then
+ move(keys, 1, n, m+1)
+ tableSort(list)
+ for i = 1, m do
+ local key = list[i]
+ keys[i] = key
+ mark[key] = true
+ end
+ n = n + m
+ end
+ local i = 0
+ return function ()
+ i = i + 1
+ local key = keys[i]
+ return key, self[key]
+ end
+ end
+
+ return setmetatable(tbl, mt)
+end
+
+--- 创建一个有序表
+---@param tbl table {optional = 'self'}
+---@return table
+function m.container(tbl)
+ return sortTable(tbl)
+end
+
+--- 读取文件
+---@param path string
+function m.loadFile(path)
+ local f, e = ioOpen(path, 'rb')
+ if not f then
+ return nil, e
+ end
+ if f:read(3) ~= '\xEF\xBB\xBF' then
+ f:seek("set")
+ end
+ local buf = f:read 'a'
+ f:close()
+ return buf
+end
+
+--- 写入文件
+---@param path string
+---@param content string
+function m.saveFile(path, content)
+ local f, e = ioOpen(path, "wb")
+
+ if f then
+ f:write(content)
+ f:close()
+ return true
+ else
+ return false, e
+ end
+end
+
+--- 计数器
+---@param init integer {optional = 'after'}
+---@param step integer {optional = 'after'}
+---@return fun():integer
+function m.counter(init, step)
+ if not step then
+ step = 1
+ end
+ local current = init and (init - 1) or 0
+ return function ()
+ current = current + step
+ return current
+ end
+end
+
+--- 排序后遍历
+---@param t table
+function m.sortPairs(t)
+ local keys = {}
+ for k in pairs(t) do
+ keys[#keys+1] = k
+ end
+ tableSort(keys)
+ local i = 0
+ return function ()
+ i = i + 1
+ local k = keys[i]
+ return k, t[k]
+ end
+end
+
+--- 深拷贝(不处理元表)
+---@param source table
+---@param target table {optional = 'self'}
+function m.deepCopy(source, target)
+ local mark = {}
+ local function copy(a, b)
+ if type(a) ~= 'table' then
+ return a
+ end
+ if mark[a] then
+ return mark[a]
+ end
+ if not b then
+ b = {}
+ end
+ mark[a] = b
+ for k, v in pairs(a) do
+ b[copy(k)] = copy(v)
+ end
+ return b
+ end
+ return copy(source, target)
+end
+
+--- 序列化
+function m.unpack(t)
+ local result = {}
+ local tid = 0
+ local cache = {}
+ local function unpack(o)
+ local id = cache[o]
+ if not id then
+ tid = tid + 1
+ id = tid
+ cache[o] = tid
+ if type(o) == 'table' then
+ local new = {}
+ result[tid] = new
+ for k, v in next, o do
+ new[unpack(k)] = unpack(v)
+ end
+ else
+ result[id] = o
+ end
+ end
+ return id
+ end
+ unpack(t)
+ return result
+end
+
+--- 反序列化
+function m.pack(t)
+ local cache = {}
+ local function pack(id)
+ local o = cache[id]
+ if o then
+ return o
+ end
+ o = t[id]
+ if type(o) == 'table' then
+ local new = {}
+ cache[id] = new
+ for k, v in next, o do
+ new[pack(k)] = pack(v)
+ end
+ return new
+ else
+ cache[id] = o
+ return o
+ end
+ end
+ return pack(1)
+end
+
+--- defer
+local deferMT = { __close = function (self) self[1]() end }
+function m.defer(callback)
+ return setmetatable({ callback }, deferMT)
+end
+
+local esc = {
+ ["'"] = [[\']],
+ ['"'] = [[\"]],
+ ['\r'] = [[\r]],
+ ['\n'] = '\\\n',
+}
+
+function m.viewString(str, quo)
+ if not quo then
+ if not str:find("'", 1, true) and str:find('"', 1, true) then
+ quo = "'"
+ else
+ quo = '"'
+ end
+ end
+ if quo == "'" then
+ return quo .. str:gsub([=[['\r\n]]=], esc) .. quo
+ elseif quo == '"' then
+ return quo .. str:gsub([=[["\r\n]]=], esc) .. quo
+ else
+ if str:find '\r' then
+ return m.viewString(str)
+ end
+ local eqnum = #quo - 2
+ local fsymb = ']' .. ('='):rep(eqnum) .. ']'
+ if not str:find(fsymb, 1, true) then
+ return quo .. str .. fsymb
+ end
+ for i = 0, 10 do
+ local fsymb = ']' .. ('='):rep(i) .. ']'
+ if not str:find(fsymb, 1, true) then
+ local ssymb = '[' .. ('='):rep(i) .. '['
+ return ssymb .. str .. fsymb
+ end
+ end
+ return m.viewString(str)
+ end
+end
+
+function m.viewLiteral(v)
+ local tp = type(v)
+ if tp == 'nil' then
+ return 'nil'
+ elseif tp == 'string' then
+ return m.viewString(v)
+ elseif tp == 'boolean' then
+ return tostring(v)
+ elseif tp == 'number' then
+ if isInteger(v) then
+ return tostring(v)
+ else
+ return formatNumber(v)
+ end
+ end
+ return nil
+end
+
+return m
diff --git a/script-beta/src/vm/dummySource.lua b/script-beta/src/vm/dummySource.lua
new file mode 100644
index 00000000..50ff13e7
--- /dev/null
+++ b/script-beta/src/vm/dummySource.lua
@@ -0,0 +1,13 @@
+local vm = require 'vm.vm'
+
+vm.librarySourceCache = setmetatable({}, { __mode = 'kv'})
+
+function vm.librarySource(lib)
+ if not vm.librarySourceCache[lib] then
+ vm.librarySourceCache[lib] = {
+ type = 'library',
+ library = lib,
+ }
+ end
+ return vm.librarySourceCache[lib]
+end
diff --git a/script-beta/src/vm/eachDef.lua b/script-beta/src/vm/eachDef.lua
new file mode 100644
index 00000000..0274cbee
--- /dev/null
+++ b/script-beta/src/vm/eachDef.lua
@@ -0,0 +1,65 @@
+local vm = require 'vm.vm'
+local guide = require 'parser.guide'
+local files = require 'files'
+
+local function checkPath(source, info)
+ if source.type == 'goto' then
+ return true
+ end
+ local src = info.source
+ local mode = guide.getPath(source, src)
+ if not mode then
+ return true
+ end
+ if mode == 'before' then
+ return false
+ end
+ return true
+end
+
+function vm.eachDef(source, callback)
+ local results = {}
+ local valueUris = {}
+ local sourceUri = guide.getRoot(source).uri
+ vm.eachRef(source, function (info)
+ if info.mode == 'declare'
+ or info.mode == 'set'
+ or info.mode == 'return'
+ or info.mode == 'value' then
+ results[#results+1] = info
+ local src = info.source
+ if info.mode == 'return' then
+ local uri = guide.getRoot(src).uri
+ valueUris[uri] = info.source
+ end
+ end
+ end)
+
+ for _, info in ipairs(results) do
+ local src = info.source
+ local destUri = guide.getRoot(src).uri
+ -- 如果是同一个文件,则检查位置关系后放行
+ if sourceUri == destUri then
+ if checkPath(source, info) then
+ callback(info)
+ end
+ goto CONTINUE
+ end
+ -- 如果是global或field,则直接放行(因为无法确定顺序)
+ if src.type == 'setindex'
+ or src.type == 'setfield'
+ or src.type == 'setmethod'
+ or src.type == 'tablefield'
+ or src.type == 'tableindex'
+ or src.type == 'setglobal' then
+ callback(info)
+ goto CONTINUE
+ end
+ -- 如果不是同一个文件,则必须在该文件 return 后才放行
+ if valueUris[destUri] then
+ callback(info)
+ goto CONTINUE
+ end
+ ::CONTINUE::
+ end
+end
diff --git a/script-beta/src/vm/eachField.lua b/script-beta/src/vm/eachField.lua
new file mode 100644
index 00000000..1d3d222d
--- /dev/null
+++ b/script-beta/src/vm/eachField.lua
@@ -0,0 +1,169 @@
+local guide = require 'parser.guide'
+local vm = require 'vm.vm'
+
+local function ofTabel(value, callback)
+ for _, field in ipairs(value) do
+ if field.type == 'tablefield'
+ or field.type == 'tableindex' then
+ callback {
+ source = field,
+ key = guide.getKeyName(field),
+ value = field.value,
+ mode = 'set',
+ }
+ end
+ end
+end
+
+local function ofENV(source, callback)
+ if source.type == 'getlocal' then
+ local parent = source.parent
+ if parent.type == 'getfield'
+ or parent.type == 'getmethod'
+ or parent.type == 'getindex' then
+ callback {
+ source = parent,
+ key = guide.getKeyName(parent),
+ mode = 'get',
+ }
+ end
+ elseif source.type == 'getglobal' then
+ callback {
+ source = source,
+ key = guide.getKeyName(source),
+ mode = 'get',
+ }
+ elseif source.type == 'setglobal' then
+ callback {
+ source = source,
+ key = guide.getKeyName(source),
+ mode = 'set',
+ value = source.value,
+ }
+ end
+end
+
+local function ofSpecialArg(source, callback)
+ local args = source.parent
+ local call = args.parent
+ local func = call.node
+ local name = func.special
+ if name == 'rawset' then
+ if args[1] == source and args[2] then
+ callback {
+ source = call,
+ key = guide.getKeyName(args[2]),
+ value = args[3],
+ mode = 'set',
+ }
+ end
+ elseif name == 'rawget' then
+ if args[1] == source and args[2] then
+ callback {
+ source = call,
+ key = guide.getKeyName(args[2]),
+ mode = 'get',
+ }
+ end
+ elseif name == 'setmetatable' then
+ if args[1] == source and args[2] then
+ vm.eachField(args[2], function (info)
+ if info.key == 's|__index' and info.value then
+ vm.eachField(info.value, callback)
+ end
+ end)
+ end
+ end
+end
+
+local function ofVar(source, callback)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ if parent.type == 'getfield'
+ or parent.type == 'getmethod'
+ or parent.type == 'getindex' then
+ callback {
+ source = parent,
+ key = guide.getKeyName(parent),
+ mode = 'get',
+ }
+ return
+ end
+ if parent.type == 'setfield'
+ or parent.type == 'setmethod'
+ or parent.type == 'setindex' then
+ callback {
+ source = parent,
+ key = guide.getKeyName(parent),
+ value = parent.value,
+ mode = 'set',
+ }
+ return
+ end
+ if parent.type == 'callargs' then
+ ofSpecialArg(source, callback)
+ end
+end
+
+local function eachField(source, callback)
+ vm.eachRef(source, function (info)
+ local src = info.source
+ if src.tag == '_ENV' then
+ if src.ref then
+ for _, ref in ipairs(src.ref) do
+ ofENV(ref, callback)
+ end
+ end
+ elseif src.type == 'getlocal'
+ or src.type == 'getglobal'
+ or src.type == 'getfield'
+ or src.type == 'getmethod'
+ or src.type == 'getindex' then
+ ofVar(src, callback)
+ elseif src.type == 'table' then
+ ofTabel(src, callback)
+ end
+ end)
+end
+
+--- 获取所有的field
+function vm.eachField(source, callback)
+ local cache = vm.cache.eachField[source]
+ if cache then
+ for i = 1, #cache do
+ local res = callback(cache[i])
+ if res ~= nil then
+ return res
+ end
+ end
+ return
+ end
+ local unlock = vm.lock('eachField', source)
+ if not unlock then
+ return
+ end
+ cache = {}
+ vm.cache.eachField[source] = cache
+ local mark = {}
+ eachField(source, function (info)
+ local src = info.source
+ if mark[src] then
+ return
+ end
+ mark[src] = true
+ cache[#cache+1] = info
+ end)
+ unlock()
+ vm.eachRef(source, function (info)
+ local src = info.source
+ vm.cache.eachField[src] = cache
+ end)
+ for i = 1, #cache do
+ local res = callback(cache[i])
+ if res ~= nil then
+ return res
+ end
+ end
+end
diff --git a/script-beta/src/vm/eachRef.lua b/script-beta/src/vm/eachRef.lua
new file mode 100644
index 00000000..cfb2bef8
--- /dev/null
+++ b/script-beta/src/vm/eachRef.lua
@@ -0,0 +1,500 @@
+local guide = require 'parser.guide'
+local files = require 'files'
+local vm = require 'vm.vm'
+
+local function ofCall(func, index, callback)
+ vm.eachRef(func, function (info)
+ local src = info.source
+ local returns
+ if src.type == 'main' or src.type == 'function' then
+ returns = src.returns
+ end
+ if returns then
+ -- 搜索函数第 index 个返回值
+ for _, rtn in ipairs(returns) do
+ local val = rtn[index]
+ if val then
+ callback {
+ source = val,
+ mode = 'return',
+ }
+ vm.eachRef(val, callback)
+ end
+ end
+ end
+ end)
+end
+
+local function ofCallSelect(call, index, callback)
+ local slc = call.parent
+ if slc.index == index then
+ vm.eachRef(slc.parent, callback)
+ return
+ end
+ if call.extParent then
+ for i = 1, #call.extParent do
+ slc = call.extParent[i]
+ if slc.index == index then
+ vm.eachRef(slc.parent, callback)
+ return
+ end
+ end
+ end
+end
+
+local function ofReturn(rtn, index, callback)
+ local func = guide.getParentFunction(rtn)
+ if not func then
+ return
+ end
+ -- 搜索函数调用的第 index 个接收值
+ if func.type == 'main' then
+ local myUri = func.uri
+ local uris = files.findLinkTo(myUri)
+ if not uris then
+ return
+ end
+ for _, uri in ipairs(uris) do
+ local ast = files.getAst(uri)
+ if ast then
+ local links = vm.getLinks(ast.ast)
+ if links then
+ for linkUri, calls in pairs(links) do
+ if files.eq(linkUri, myUri) then
+ for i = 1, #calls do
+ ofCallSelect(calls[i], 1, callback)
+ end
+ end
+ end
+ end
+ end
+ end
+ else
+ vm.eachRef(func, function (info)
+ local source = info.source
+ local call = source.parent
+ if not call or call.type ~= 'call' then
+ return
+ end
+ ofCallSelect(call, index, callback)
+ end)
+ end
+end
+
+local function ofSpecialCall(call, func, index, callback)
+ local name = func.special
+ if name == 'setmetatable' then
+ if index == 1 then
+ local args = call.args
+ if args[1] then
+ vm.eachRef(args[1], callback)
+ end
+ if args[2] then
+ vm.eachField(args[2], function (info)
+ if info.key == 's|__index' then
+ vm.eachRef(info.source, callback)
+ if info.value then
+ vm.eachRef(info.value, callback)
+ end
+ end
+ end)
+ end
+ end
+ elseif name == 'require' then
+ if index == 1 then
+ local result = vm.getLinkUris(call)
+ if result then
+ local myUri = guide.getRoot(call).uri
+ for _, uri in ipairs(result) do
+ if not files.eq(uri, myUri) then
+ local ast = files.getAst(uri)
+ if ast then
+ ofCall(ast.ast, 1, callback)
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+local function ofValue(value, callback)
+ if value.type == 'select' then
+ -- 检查函数返回值
+ local call = value.vararg
+ if call.type == 'call' then
+ ofCall(call.node, value.index, callback)
+ ofSpecialCall(call, call.node, value.index, callback)
+ end
+ return
+ end
+
+ if value.type == 'table'
+ or value.type == 'string'
+ or value.type == 'number'
+ or value.type == 'boolean'
+ or value.type == 'nil'
+ or value.type == 'function' then
+ callback {
+ source = value,
+ mode = 'value',
+ }
+ end
+
+ vm.eachRef(value, callback)
+
+ local parent = value.parent
+ if parent.type == 'local'
+ or parent.type == 'setglobal'
+ or parent.type == 'setlocal'
+ or parent.type == 'setfield'
+ or parent.type == 'setmethod'
+ or parent.type == 'setindex'
+ or parent.type == 'tablefield'
+ or parent.type == 'tableindex' then
+ if parent.value == value then
+ vm.eachRef(parent, callback)
+ end
+ end
+ if parent.type == 'return' then
+ for i = 1, #parent do
+ if parent[i] == value then
+ ofReturn(parent, i, callback)
+ break
+ end
+ end
+ end
+end
+
+local function ofSelf(loc, callback)
+ -- self 的2个特殊引用位置:
+ -- 1. 当前方法定义时的对象(mt)
+ local method = loc.method
+ local node = method.node
+ vm.eachRef(node, callback)
+ -- 2. 调用该方法时传入的对象
+end
+
+--- 自己作为赋值的值
+local function asValue(source, callback)
+ local parent = source.parent
+ if parent and parent.value == source then
+ if guide.getName(parent) == '__index' then
+ if parent.type == 'tablefield'
+ or parent.type == 'tableindex' then
+ local t = parent.parent
+ local args = t.parent
+ if args[2] == t then
+ local call = args.parent
+ local func = call.node
+ if func.special == 'setmetatable' then
+ vm.eachRef(args[1], callback)
+ end
+ end
+ end
+ end
+ end
+end
+
+local function getCallRecvs(call)
+ local parent = call.parent
+ if parent.type ~= 'select' then
+ return nil
+ end
+ local exParent = call.exParent
+ local recvs = {}
+ recvs[1] = parent.parent
+ if exParent then
+ for _, p in ipairs(exParent) do
+ recvs[#recvs+1] = p.parent
+ end
+ end
+ return recvs
+end
+
+--- 自己作为函数的参数
+local function asArg(source, callback)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ if parent.type == 'callargs' then
+ local call = parent.parent
+ local func = call.node
+ local name = func.special
+ if name == 'setmetatable' then
+ if parent[1] == source then
+ if parent[2] then
+ vm.eachField(parent[2], function (info)
+ if info.key == 's|__index' then
+ vm.eachRef(info.source, callback)
+ if info.value then
+ vm.eachRef(info.value, callback)
+ end
+ end
+ end)
+ end
+ end
+ local recvs = getCallRecvs(call)
+ if recvs and recvs[1] then
+ vm.eachRef(recvs[1], callback)
+ end
+ end
+ end
+end
+
+local function ofLocal(loc, callback)
+ -- 方法中的 self 使用了一个虚拟的定义位置
+ if loc.tag ~= 'self' then
+ callback {
+ source = loc,
+ mode = 'declare',
+ }
+ end
+ if loc.ref then
+ for _, ref in ipairs(loc.ref) do
+ if ref.type == 'getlocal' then
+ callback {
+ source = ref,
+ mode = 'get',
+ }
+ asValue(ref, callback)
+ elseif ref.type == 'setlocal' then
+ callback {
+ source = ref,
+ mode = 'set',
+ }
+ if ref.value then
+ ofValue(ref.value, callback)
+ end
+ end
+ end
+ end
+ if loc.tag == 'self' then
+ ofSelf(loc, callback)
+ end
+ if loc.value then
+ ofValue(loc.value, callback)
+ end
+ if loc.tag == '_ENV' and loc.ref then
+ for _, ref in ipairs(loc.ref) do
+ if ref.type == 'getlocal' then
+ local parent = ref.parent
+ if parent.type == 'getfield'
+ or parent.type == 'getindex' then
+ if guide.getKeyName(parent) == '_G' then
+ callback {
+ source = parent,
+ mode = 'get',
+ }
+ end
+ end
+ elseif ref.type == 'getglobal' then
+ if guide.getName(ref) == '_G' then
+ callback {
+ source = ref,
+ mode = 'get',
+ }
+ end
+ end
+ end
+ end
+end
+
+local function ofGlobal(source, callback)
+ local key = guide.getKeyName(source)
+ local node = source.node
+ if node.tag == '_ENV' then
+ local uris = files.findGlobals(key)
+ for _, uri in ipairs(uris) do
+ local ast = files.getAst(uri)
+ local globals = vm.getGlobals(ast.ast)
+ if globals[key] then
+ for _, info in ipairs(globals[key]) do
+ callback(info)
+ if info.value then
+ ofValue(info.value, callback)
+ end
+ end
+ end
+ end
+ else
+ vm.eachField(node, function (info)
+ if key == info.key then
+ callback {
+ source = info.source,
+ mode = info.mode,
+ }
+ if info.value then
+ ofValue(info.value, callback)
+ end
+ end
+ end)
+ end
+end
+
+local function ofField(source, callback)
+ local parent = source.parent
+ local key = guide.getKeyName(source)
+ if parent.type == 'tablefield'
+ or parent.type == 'tableindex' then
+ local tbl = parent.parent
+ vm.eachField(tbl, function (info)
+ if key == info.key then
+ callback {
+ source = info.source,
+ mode = info.mode,
+ }
+ if info.value then
+ ofValue(info.value, callback)
+ end
+ end
+ end)
+ else
+ local node = parent.node
+ vm.eachField(node, function (info)
+ if key == info.key then
+ callback {
+ source = info.source,
+ mode = info.mode,
+ }
+ if info.value then
+ ofValue(info.value, callback)
+ end
+ end
+ end)
+ end
+end
+
+local function ofLiteral(source, callback)
+ local parent = source.parent
+ if not parent then
+ return
+ end
+ if parent.type == 'setindex'
+ or parent.type == 'getindex'
+ or parent.type == 'tableindex' then
+ ofField(source, callback)
+ end
+end
+
+local function ofLabel(source, callback)
+ callback {
+ source = source,
+ mode = 'set',
+ }
+ if source.ref then
+ for _, ref in ipairs(source.ref) do
+ callback {
+ source = ref,
+ mode = 'get',
+ }
+ end
+ end
+end
+
+local function ofGoTo(source, callback)
+ local name = source[1]
+ local label = guide.getLabel(source, name)
+ if label then
+ ofLabel(label, callback)
+ end
+end
+
+local function ofMain(source, callback)
+ callback {
+ source = source,
+ mode = 'main',
+ }
+end
+
+local function eachRef(source, callback)
+ local stype = source.type
+ if stype == 'local' then
+ ofLocal(source, callback)
+ elseif stype == 'getlocal'
+ or stype == 'setlocal' then
+ ofLocal(source.node, callback)
+ elseif stype == 'setglobal'
+ or stype == 'getglobal' then
+ ofGlobal(source, callback)
+ elseif stype == 'field'
+ or stype == 'method' then
+ ofField(source, callback)
+ elseif stype == 'setfield'
+ or stype == 'getfield' then
+ ofField(source.field, callback)
+ elseif stype == 'setmethod'
+ or stype == 'getmethod' then
+ ofField(source.method, callback)
+ elseif stype == 'number'
+ or stype == 'boolean'
+ or stype == 'string' then
+ ofLiteral(source, callback)
+ elseif stype == 'goto' then
+ ofGoTo(source, callback)
+ elseif stype == 'label' then
+ ofLabel(source, callback)
+ elseif stype == 'table'
+ or stype == 'function' then
+ ofValue(source, callback)
+ elseif stype == 'main' then
+ ofMain(source, callback)
+ end
+ asArg(source, callback)
+end
+
+--- 判断2个对象是否拥有相同的引用
+function vm.isSameRef(a, b)
+ local cache = vm.cache.eachRef[a]
+ if cache then
+ -- 相同引用的source共享同一份cache
+ return cache == vm.cache.eachRef[b]
+ else
+ return vm.eachRef(a, function (info)
+ if info.source == b then
+ return true
+ end
+ end) or false
+ end
+end
+
+--- 获取所有的引用
+function vm.eachRef(source, callback)
+ local cache = vm.cache.eachRef[source]
+ if cache then
+ for i = 1, #cache do
+ local res = callback(cache[i])
+ if res ~= nil then
+ return res
+ end
+ end
+ return
+ end
+ local unlock = vm.lock('eachRef', source)
+ if not unlock then
+ return
+ end
+ cache = {}
+ vm.cache.eachRef[source] = cache
+ local mark = {}
+ eachRef(source, function (info)
+ local src = info.source
+ if mark[src] then
+ return
+ end
+ mark[src] = true
+ cache[#cache+1] = info
+ end)
+ unlock()
+ for i = 1, #cache do
+ local src = cache[i].source
+ vm.cache.eachRef[src] = cache
+ end
+ for i = 1, #cache do
+ local res = callback(cache[i])
+ if res ~= nil then
+ return res
+ end
+ end
+end
diff --git a/script-beta/src/vm/getGlobal.lua b/script-beta/src/vm/getGlobal.lua
new file mode 100644
index 00000000..373c907e
--- /dev/null
+++ b/script-beta/src/vm/getGlobal.lua
@@ -0,0 +1,6 @@
+local vm = require 'vm.vm'
+
+function vm.getGlobal(source)
+ vm.getGlobals(source)
+ return vm.cache.getGlobal[source]
+end
diff --git a/script-beta/src/vm/getGlobals.lua b/script-beta/src/vm/getGlobals.lua
new file mode 100644
index 00000000..699dd270
--- /dev/null
+++ b/script-beta/src/vm/getGlobals.lua
@@ -0,0 +1,45 @@
+local guide = require 'parser.guide'
+local vm = require 'vm.vm'
+
+local function getGlobals(root)
+ local env = guide.getENV(root)
+ local cache = {}
+ local mark = {}
+ vm.eachField(env, function (info)
+ local src = info.source
+ if mark[src] then
+ return
+ end
+ mark[src] = true
+ local name = info.key
+ if not name then
+ return
+ end
+ if not cache[name] then
+ cache[name] = {
+ key = name,
+ mode = {},
+ }
+ end
+ cache[name][#cache[name]+1] = info
+ cache[name].mode[info.mode] = true
+ vm.cache.getGlobal[src] = name
+ end)
+ return cache
+end
+
+function vm.getGlobals(source)
+ source = guide.getRoot(source)
+ local cache = vm.cache.getGlobals[source]
+ if cache ~= nil then
+ return cache
+ end
+ local unlock = vm.lock('getGlobals', source)
+ if not unlock then
+ return nil
+ end
+ cache = getGlobals(source) or false
+ vm.cache.getGlobals[source] = cache
+ unlock()
+ return cache
+end
diff --git a/script-beta/src/vm/getLibrary.lua b/script-beta/src/vm/getLibrary.lua
new file mode 100644
index 00000000..fd05347e
--- /dev/null
+++ b/script-beta/src/vm/getLibrary.lua
@@ -0,0 +1,89 @@
+local vm = require 'vm.vm'
+local library = require 'library'
+local guide = require 'parser.guide'
+
+local function checkStdLibrary(source)
+ local globalName = vm.getGlobal(source)
+ if not globalName then
+ return nil
+ end
+ local name = globalName:match '^s|(.+)$'
+ if library.global[name] then
+ return library.global[name]
+ end
+end
+
+local function getLibInNode(source, nodeLib)
+ if not nodeLib then
+ return nil
+ end
+ if not nodeLib.child then
+ return nil
+ end
+ local key = guide.getName(source)
+ local defLib = nodeLib.child[key]
+ return defLib
+end
+
+local function getNodeAsTable(source)
+ local node = source.node
+ local nodeGlobalName = vm.getGlobal(node)
+ if not nodeGlobalName then
+ return nil
+ end
+ local nodeName = nodeGlobalName:match '^s|(.+)$'
+ return getLibInNode(source, library.global[nodeName])
+end
+
+local function getNodeAsObject(source)
+ local node = source.node
+ local values = vm.getValue(node)
+ if not values then
+ return nil
+ end
+ for i = 1, #values do
+ local value = values[i]
+ local type = value.type
+ local nodeLib = library.object[type]
+ local lib = getLibInNode(source, nodeLib)
+ if lib then
+ return lib
+ end
+ end
+ return nil
+end
+
+local function checkNode(source)
+ if source.type ~= 'getfield'
+ and source.type ~= 'getmethod'
+ and source.type ~= 'getindex' then
+ return nil
+ end
+ return getNodeAsTable(source)
+ or getNodeAsObject(source)
+end
+
+local function getLibrary(source)
+ local lib = checkStdLibrary(source)
+ if lib then
+ return lib
+ end
+ return checkNode(source) or vm.eachRef(source, function (info)
+ return checkNode(info.source)
+ end)
+end
+
+function vm.getLibrary(source)
+ local cache = vm.cache.getLibrary[source]
+ if cache ~= nil then
+ return cache
+ end
+ local unlock = vm.lock('getLibrary', source)
+ if not unlock then
+ return
+ end
+ cache = getLibrary(source) or false
+ vm.cache.getLibrary[source] = cache
+ unlock()
+ return cache
+end
diff --git a/script-beta/src/vm/getLinks.lua b/script-beta/src/vm/getLinks.lua
new file mode 100644
index 00000000..6875771f
--- /dev/null
+++ b/script-beta/src/vm/getLinks.lua
@@ -0,0 +1,48 @@
+local guide = require 'parser.guide'
+local vm = require 'vm.vm'
+
+local function getLinks(root)
+ local cache = {}
+ local ok
+ guide.eachSpecialOf(root, 'require', function (source)
+ local call = source.parent
+ if call.type == 'call' then
+ local uris = vm.getLinkUris(call)
+ if uris then
+ ok = true
+ for i = 1, #uris do
+ local uri = uris[i]
+ if not cache[uri] then
+ cache[uri] = {}
+ end
+ cache[uri][#cache[uri]+1] = call
+ end
+ end
+ end
+ end)
+ if not ok then
+ return nil
+ end
+ return cache
+end
+
+function vm.getLinks(source)
+ source = guide.getRoot(source)
+ local cache = vm.cache.getLinks[source]
+ if cache ~= nil then
+ return cache
+ end
+ local unlock = vm.lock('getLinks', source)
+ if not unlock then
+ return nil
+ end
+ local clock = os.clock()
+ cache = getLinks(source) or false
+ local passed = os.clock() - clock
+ if passed > 0.1 then
+ log.warn(('getLinks takes [%.3f] sec!'):format(passed))
+ end
+ vm.cache.getLinks[source] = cache
+ unlock()
+ return cache
+end
diff --git a/script-beta/src/vm/getValue.lua b/script-beta/src/vm/getValue.lua
new file mode 100644
index 00000000..ee486a54
--- /dev/null
+++ b/script-beta/src/vm/getValue.lua
@@ -0,0 +1,895 @@
+local vm = require 'vm.vm'
+
+local typeSort = {
+ ['boolean'] = 1,
+ ['string'] = 2,
+ ['integer'] = 3,
+ ['number'] = 4,
+ ['table'] = 5,
+ ['function'] = 6,
+ ['nil'] = math.maxinteger,
+}
+
+NIL = setmetatable({'<nil>'}, { __tostring = function () return 'nil' end })
+
+local function merge(t, b)
+ if not t then
+ t = {}
+ end
+ if not b then
+ return t
+ end
+ for i = 1, #b do
+ local o = b[i]
+ if not t[o] then
+ t[o] = true
+ t[#t+1] = o
+ end
+ end
+ return t
+end
+
+local function alloc(o)
+ -- TODO
+ assert(o.type)
+ if type(o.type) == 'table' then
+ local values = {}
+ for i = 1, #o.type do
+ local sub = {
+ type = o.type[i],
+ value = o.value,
+ source = o.source,
+ }
+ values[i] = sub
+ values[sub] = true
+ end
+ return values
+ else
+ return {
+ [1] = o,
+ [o] = true,
+ }
+ end
+end
+
+local function insert(t, o)
+ if not o then
+ return
+ end
+ if not t[o] then
+ t[o] = true
+ t[#t+1] = o
+ end
+ return t
+end
+
+local function checkLiteral(source)
+ if source.type == 'string' then
+ return alloc {
+ type = 'string',
+ value = source[1],
+ source = source,
+ }
+ elseif source.type == 'nil' then
+ return alloc {
+ type = 'nil',
+ value = NIL,
+ source = source,
+ }
+ elseif source.type == 'boolean' then
+ return alloc {
+ type = 'boolean',
+ value = source[1],
+ source = source,
+ }
+ elseif source.type == 'number' then
+ if math.type(source[1]) == 'integer' then
+ return alloc {
+ type = 'integer',
+ value = source[1],
+ source = source,
+ }
+ else
+ return alloc {
+ type = 'number',
+ value = source[1],
+ source = source,
+ }
+ end
+ elseif source.type == 'table' then
+ return alloc {
+ type = 'table',
+ source = source,
+ }
+ elseif source.type == 'function' then
+ return alloc {
+ type = 'function',
+ source = source,
+ }
+ end
+end
+
+local function checkUnary(source)
+ if source.type ~= 'unary' then
+ return
+ end
+ local op = source.op
+ if op.type == 'not' then
+ local checkTrue = vm.checkTrue(source[1])
+ local value = nil
+ if checkTrue == true then
+ value = false
+ elseif checkTrue == false then
+ value = true
+ end
+ return alloc {
+ type = 'boolean',
+ value = value,
+ source = source,
+ }
+ elseif op.type == '#' then
+ return alloc {
+ type = 'integer',
+ source = source,
+ }
+ elseif op.type == '~' then
+ local l = vm.getLiteral(source[1], 'integer')
+ return alloc {
+ type = 'integer',
+ value = l and ~l or nil,
+ source = source,
+ }
+ elseif op.type == '-' then
+ local v = vm.getLiteral(source[1], 'integer')
+ if v then
+ return alloc {
+ type = 'integer',
+ value = - v,
+ source = source,
+ }
+ end
+ v = vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = v and -v or nil,
+ source = source,
+ }
+ end
+end
+
+local function checkBinary(source)
+ if source.type ~= 'binary' then
+ return
+ end
+ local op = source.op
+ if op.type == 'and' then
+ local isTrue = vm.checkTrue(source[1])
+ if isTrue == true then
+ return vm.getValue(source[2])
+ elseif isTrue == false then
+ return vm.getValue(source[1])
+ else
+ return merge(
+ vm.getValue(source[1]),
+ vm.getValue(source[2])
+ )
+ end
+ elseif op.type == 'or' then
+ local isTrue = vm.checkTrue(source[1])
+ if isTrue == true then
+ return vm.getValue(source[1])
+ elseif isTrue == false then
+ return vm.getValue(source[2])
+ else
+ return merge(
+ vm.getValue(source[1]),
+ vm.getValue(source[2])
+ )
+ end
+ elseif op.type == '==' then
+ local value = vm.isSameValue(source[1], source[2])
+ if value ~= nil then
+ return alloc {
+ type = 'boolean',
+ value = value,
+ source = source,
+ }
+ end
+ local isSame = vm.isSameRef(source[1], source[2])
+ if isSame == true then
+ value = true
+ else
+ value = nil
+ end
+ return alloc {
+ type = 'boolean',
+ value = value,
+ source = source,
+ }
+ elseif op.type == '~=' then
+ local value = vm.isSameValue(source[1], source[2])
+ if value ~= nil then
+ return alloc {
+ type = 'boolean',
+ value = not value,
+ source = source,
+ }
+ end
+ local isSame = vm.isSameRef(source[1], source[2])
+ if isSame == true then
+ value = false
+ else
+ value = nil
+ end
+ return alloc {
+ type = 'boolean',
+ value = value,
+ source = source,
+ }
+ elseif op.type == '<=' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 <= v2
+ end
+ return alloc {
+ type = 'boolean',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '>=' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 >= v2
+ end
+ return alloc {
+ type = 'boolean',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '<' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 < v2
+ end
+ return alloc {
+ type = 'boolean',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '>' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 > v2
+ end
+ return alloc {
+ type = 'boolean',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '|' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ local v
+ if v1 and v2 then
+ v = v1 | v2
+ end
+ return alloc {
+ type = 'integer',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '~' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ local v
+ if v1 and v2 then
+ v = v1 ~ v2
+ end
+ return alloc {
+ type = 'integer',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '&' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ local v
+ if v1 and v2 then
+ v = v1 & v2
+ end
+ return alloc {
+ type = 'integer',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '<<' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ local v
+ if v1 and v2 then
+ v = v1 << v2
+ end
+ return alloc {
+ type = 'integer',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '>>' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ local v
+ if v1 and v2 then
+ v = v1 >> v2
+ end
+ return alloc {
+ type = 'integer',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '..' then
+ local v1 = vm.getLiteral(source[1], 'string')
+ local v2 = vm.getLiteral(source[2], 'string')
+ local v
+ if v1 and v2 then
+ v = v1 .. v2
+ end
+ return alloc {
+ type = 'string',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '^' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 ^ v2
+ end
+ return alloc {
+ type = 'number',
+ value = v,
+ source = source,
+ }
+ elseif op.type == '/' then
+ local v1 = vm.getLiteral(source[1], 'integer') or vm.getLiteral(source[1], 'number')
+ local v2 = vm.getLiteral(source[2], 'integer') or vm.getLiteral(source[2], 'number')
+ local v
+ if v1 and v2 then
+ v = v1 > v2
+ end
+ return alloc {
+ type = 'number',
+ value = v,
+ source = source,
+ }
+ -- 其他数学运算根据2侧的值决定,当2侧的值均为整数时返回整数
+ elseif op.type == '+' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ if v1 and v2 then
+ return alloc {
+ type = 'integer',
+ value = v1 + v2,
+ source = source,
+ }
+ end
+ v1 = v1 or vm.getLiteral(source[1], 'number')
+ v2 = v2 or vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = (v1 and v2) and (v1 + v2) or nil,
+ source = source,
+ }
+ elseif op.type == '-' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ if v1 and v2 then
+ return alloc {
+ type = 'integer',
+ value = v1 - v2,
+ source = source,
+ }
+ end
+ v1 = v1 or vm.getLiteral(source[1], 'number')
+ v2 = v2 or vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = (v1 and v2) and (v1 - v2) or nil,
+ source = source,
+ }
+ elseif op.type == '*' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ if v1 and v2 then
+ return alloc {
+ type = 'integer',
+ value = v1 * v2,
+ source = source,
+ }
+ end
+ v1 = v1 or vm.getLiteral(source[1], 'number')
+ v2 = v2 or vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = (v1 and v2) and (v1 * v2) or nil,
+ source = source,
+ }
+ elseif op.type == '%' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ if v1 and v2 then
+ return alloc {
+ type = 'integer',
+ value = v1 % v2,
+ source = source,
+ }
+ end
+ v1 = v1 or vm.getLiteral(source[1], 'number')
+ v2 = v2 or vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = (v1 and v2) and (v1 % v2) or nil,
+ source = source,
+ }
+ elseif op.type == '//' then
+ local v1 = vm.getLiteral(source[1], 'integer')
+ local v2 = vm.getLiteral(source[2], 'integer')
+ if v1 and v2 then
+ return alloc {
+ type = 'integer',
+ value = v1 // v2,
+ source = source,
+ }
+ end
+ v1 = v1 or vm.getLiteral(source[1], 'number')
+ v2 = v2 or vm.getLiteral(source[1], 'number')
+ return alloc {
+ type = 'number',
+ value = (v1 and v2) and (v1 // v2) or nil,
+ source = source,
+ }
+ end
+end
+
+local function checkValue(source)
+ if source.value then
+ return vm.getValue(source.value)
+ end
+ if source.type == 'paren' then
+ return vm.getValue(source.exp)
+ end
+end
+
+local function hasTypeInResults(results, type)
+ for i = 1, #results do
+ if results[i].type == type then
+ return true
+ end
+ end
+ return false
+end
+
+local function inferByCall(results, source)
+ if #results ~= 0 then
+ return
+ end
+ if not source.parent then
+ return
+ end
+ if source.parent.type ~= 'call' then
+ return
+ end
+ if source.parent.node == source then
+ insert(results, {
+ type = 'function',
+ source = source,
+ })
+ return
+ end
+end
+
+local function inferByGetTable(results, source)
+ if #results ~= 0 then
+ return
+ end
+ local next = source.next
+ if not next then
+ return
+ end
+ if next.type == 'getfield'
+ or next.type == 'getindex'
+ or next.type == 'getmethod'
+ or next.type == 'setfield'
+ or next.type == 'setindex'
+ or next.type == 'setmethod' then
+ insert(results, {
+ type = 'table',
+ source = source,
+ })
+ end
+end
+
+local function checkDef(results, source)
+ vm.eachDef(source, function (info)
+ local src = info.source
+ local tp = vm.getValue(src)
+ if tp then
+ merge(results, tp)
+ end
+ end)
+end
+
+local function checkLibrary(source)
+ local lib = vm.getLibrary(source)
+ if not lib then
+ return nil
+ end
+ return alloc {
+ type = lib.type,
+ value = lib.value,
+ source = vm.librarySource(lib),
+ }
+end
+
+local function checkLibraryReturn(source)
+ if source.type ~= 'select' then
+ return nil
+ end
+ local index = source.index
+ local call = source.vararg
+ if call.type ~= 'call' then
+ return nil
+ end
+ local func = call.node
+ local lib = vm.getLibrary(func)
+ if not lib then
+ return nil
+ end
+ if lib.type ~= 'function' then
+ return nil
+ end
+ if not lib.returns then
+ return nil
+ end
+ local rtn = lib.returns[index]
+ if not rtn then
+ return nil
+ end
+ return alloc {
+ type = rtn.type,
+ value = rtn.value,
+ source = vm.librarySource(rtn),
+ }
+end
+
+local function checkLibraryArg(source)
+ local args = source.parent
+ if not args then
+ return
+ end
+ if args.type ~= 'callargs' then
+ return
+ end
+ local call = args.parent
+ if not call then
+ return
+ end
+ local func = call.node
+ local index
+ for i = 1, #args do
+ if args[i] == source then
+ index = i
+ break
+ end
+ end
+ if not index then
+ return
+ end
+ local lib = vm.getLibrary(func)
+ local arg = lib and lib.args and lib.args[index]
+ if not arg then
+ return
+ end
+ if arg.type == '...' then
+ return
+ end
+ return alloc {
+ type = arg.type,
+ value = arg.value,
+ source = vm.librarySource(arg),
+ }
+end
+
+local function inferByUnary(results, source)
+ if #results ~= 0 then
+ return
+ end
+ local parent = source.parent
+ if not parent or parent.type ~= 'unary' then
+ return
+ end
+ local op = parent.op
+ if op.type == '#' then
+ insert(results, {
+ type = 'string',
+ source = vm.librarySource(source)
+ })
+ insert(results, {
+ type = 'table',
+ source = vm.librarySource(source)
+ })
+ elseif op.type == '~' then
+ insert(results, {
+ type = 'integer',
+ source = vm.librarySource(source)
+ })
+ elseif op.type == '-' then
+ insert(results, {
+ type = 'number',
+ source = vm.librarySource(source)
+ })
+ end
+end
+
+local function inferByBinary(results, source)
+ if #results ~= 0 then
+ return
+ end
+ local parent = source.parent
+ if not parent or parent.type ~= 'binary' then
+ return
+ end
+ local op = parent.op
+ if op.type == '<='
+ or op.type == '>='
+ or op.type == '<'
+ or op.type == '>'
+ or op.type == '^'
+ or op.type == '/'
+ or op.type == '+'
+ or op.type == '-'
+ or op.type == '*'
+ or op.type == '%' then
+ insert(results, {
+ type = 'number',
+ source = vm.librarySource(source)
+ })
+ elseif op.type == '|'
+ or op.type == '~'
+ or op.type == '&'
+ or op.type == '<<'
+ or op.type == '>>'
+ -- 整数的可能性比较高
+ or op.type == '//' then
+ insert(results, {
+ type = 'integer',
+ source = vm.librarySource(source)
+ })
+ elseif op.type == '..' then
+ insert(results, {
+ type = 'string',
+ source = vm.librarySource(source)
+ })
+ end
+end
+
+local function inferBySetOfLocal(results, source)
+ if source.ref then
+ for i = 1, #source.ref do
+ local ref = source.ref[i]
+ if ref.type == 'setlocal' then
+ break
+ end
+ merge(results, vm.getValue(ref))
+ end
+ end
+end
+
+local function inferBySet(results, source)
+ if #results ~= 0 then
+ return
+ end
+ if source.type == 'local' then
+ inferBySetOfLocal(results, source)
+ elseif source.type == 'setlocal'
+ or source.type == 'getlocal' then
+ inferBySetOfLocal(results, source.node)
+ end
+end
+
+local function getValue(source)
+ local results = checkLiteral(source)
+ or checkValue(source)
+ or checkUnary(source)
+ or checkBinary(source)
+ or checkLibrary(source)
+ or checkLibraryReturn(source)
+ or checkLibraryArg(source)
+ if results then
+ return results
+ end
+
+ results = {}
+ checkDef(results, source)
+ inferBySet(results, source)
+ inferByCall(results, source)
+ inferByGetTable(results, source)
+ inferByUnary(results, source)
+ inferByBinary(results, source)
+
+ if #results == 0 then
+ return nil
+ end
+
+ return results
+end
+
+function vm.checkTrue(source)
+ local values = vm.getValue(source)
+ if not values then
+ return
+ end
+ -- 当前认为的结果
+ local current
+ for i = 1, #values do
+ -- 新的结果
+ local new
+ local v = values[i]
+ if v.type == 'nil' then
+ new = false
+ elseif v.type == 'boolean' then
+ if v.value == true then
+ new = true
+ elseif v.value == false then
+ new = false
+ end
+ end
+ if new ~= nil then
+ if current == nil then
+ current = new
+ else
+ -- 如果2个结果完全相反,则返回 nil 表示不确定
+ if new ~= current then
+ return nil
+ end
+ end
+ end
+ end
+ return current
+end
+
+--- 获取特定类型的字面量值
+function vm.getLiteral(source, type)
+ local values = vm.getValue(source)
+ if not values then
+ return nil
+ end
+ for i = 1, #values do
+ local v = values[i]
+ if v.value ~= nil then
+ if type == nil or v.type == type then
+ return v.value
+ end
+ end
+ end
+ return nil
+end
+
+function vm.isSameValue(a, b)
+ local valuesA = vm.getValue(a)
+ local valuesB = vm.getValue(b)
+ if not valuesA or not valuesB then
+ return false
+ end
+ if valuesA == valuesB then
+ return true
+ end
+ local values = {}
+ for i = 1, #valuesA do
+ local value = valuesA[i]
+ local literal = value.value
+ if literal then
+ values[literal] = false
+ end
+ end
+ for i = 1, #valuesB do
+ local value = valuesA[i]
+ local literal = value.value
+ if literal then
+ if values[literal] == nil then
+ return false
+ end
+ values[literal] = true
+ end
+ end
+ for k, v in pairs(values) do
+ if v == false then
+ return false
+ end
+ end
+ return true
+end
+
+--- 是否包含某种类型
+function vm.hasType(source, type)
+ local values = vm.getValue(source)
+ if not values then
+ return false
+ end
+ for i = 1, #values do
+ local value = values[i]
+ if value.type == type then
+ return true
+ end
+ end
+ return false
+end
+
+function vm.viewType(values)
+ if not values then
+ return 'any'
+ end
+ local types = {}
+ for i = 1, #values do
+ local tp = values[i].type
+ if not types[tp] then
+ types[tp] = true
+ types[#types+1] = tp
+ end
+ end
+ if #types == 0 then
+ return 'any'
+ end
+ if #types == 1 then
+ return types[1]
+ end
+ table.sort(types, function (a, b)
+ local sa = typeSort[a]
+ local sb = typeSort[b]
+ if sa and sb then
+ return sa < sb
+ end
+ if not sa and not sb then
+ return a < b
+ end
+ if sa and not sb then
+ return true
+ end
+ if not sa and sb then
+ return false
+ end
+ return false
+ end)
+ return table.concat(types, '|')
+end
+
+function vm.getType(source)
+ local values = vm.getValue(source)
+ return vm.viewType(values)
+end
+
+function vm.getValue(source)
+ if not source then
+ return
+ end
+ local cache = vm.cache.getValue[source]
+ if cache ~= nil then
+ return cache
+ end
+ local unlock = vm.lock('getValue', source)
+ if not unlock then
+ return
+ end
+ cache = getValue(source) or false
+ vm.cache.getValue[source] = cache
+ unlock()
+ return cache
+end
diff --git a/script-beta/src/vm/init.lua b/script-beta/src/vm/init.lua
new file mode 100644
index 00000000..4249de3d
--- /dev/null
+++ b/script-beta/src/vm/init.lua
@@ -0,0 +1,11 @@
+local vm = require 'vm.vm'
+require 'vm.eachField'
+require 'vm.eachRef'
+require 'vm.eachDef'
+require 'vm.getGlobals'
+require 'vm.getLinks'
+require 'vm.getGlobal'
+require 'vm.getLibrary'
+require 'vm.getValue'
+require 'vm.dummySource'
+return vm
diff --git a/script-beta/src/vm/special.lua b/script-beta/src/vm/special.lua
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/script-beta/src/vm/special.lua
diff --git a/script-beta/src/vm/vm.lua b/script-beta/src/vm/vm.lua
new file mode 100644
index 00000000..23a691df
--- /dev/null
+++ b/script-beta/src/vm/vm.lua
@@ -0,0 +1,81 @@
+local guide = require 'parser.guide'
+local util = require 'utility'
+
+local setmetatable = setmetatable
+local assert = assert
+local require = require
+local type = type
+
+_ENV = nil
+
+local specials = {
+ ['_G'] = true,
+ ['rawset'] = true,
+ ['rawget'] = true,
+ ['setmetatable'] = true,
+ ['require'] = true,
+ ['dofile'] = true,
+ ['loadfile'] = true,
+}
+
+---@class vm
+local m = {}
+
+function m.lock(tp, source)
+ if m.locked[tp][source] then
+ return nil
+ end
+ m.locked[tp][source] = true
+ return function ()
+ m.locked[tp][source] = nil
+ end
+end
+
+--- 获取link的uri
+function m.getLinkUris(call)
+ local workspace = require 'workspace'
+ local func = call.node
+ local name = func.special
+ if name == 'require' then
+ local args = call.args
+ if not args[1] then
+ return nil
+ end
+ local literal = guide.getLiteral(args[1])
+ if type(literal) ~= 'string' then
+ return nil
+ end
+ return workspace.findUrisByRequirePath(literal, true)
+ end
+end
+
+m.cacheTracker = setmetatable({}, { __mode = 'kv' })
+
+--- 刷新缓存
+function m.refreshCache()
+ if m.cache then
+ m.cache.dead = true
+ end
+ m.cache = {
+ eachRef = {},
+ eachField = {},
+ getGlobals = {},
+ getLinks = {},
+ getGlobal = {},
+ specialName = {},
+ getLibrary = {},
+ getValue = {},
+ specials = nil,
+ }
+ m.locked = {
+ eachRef = {},
+ eachField = {},
+ getGlobals = {},
+ getLinks = {},
+ getLibrary = {},
+ getValue = {},
+ }
+ m.cacheTracker[m.cache] = true
+end
+
+return m
diff --git a/script-beta/src/workspace/init.lua b/script-beta/src/workspace/init.lua
new file mode 100644
index 00000000..7cbe15d7
--- /dev/null
+++ b/script-beta/src/workspace/init.lua
@@ -0,0 +1,3 @@
+local workspace = require 'workspace.workspace'
+
+return workspace
diff --git a/script-beta/src/workspace/workspace.lua b/script-beta/src/workspace/workspace.lua
new file mode 100644
index 00000000..37ec2d7b
--- /dev/null
+++ b/script-beta/src/workspace/workspace.lua
@@ -0,0 +1,194 @@
+local pub = require 'pub'
+local fs = require 'bee.filesystem'
+local furi = require 'file-uri'
+local files = require 'files'
+local config = require 'config'
+local glob = require 'glob'
+local platform = require 'bee.platform'
+local await = require 'await'
+local diagnostic = require 'provider.diagnostic'
+
+local m = {}
+m.type = 'workspace'
+m.ignoreVersion = -1
+m.ignoreMatcher = nil
+
+--- 初始化工作区
+function m.init(name, uri)
+ m.name = name
+ m.uri = uri
+ m.path = furi.decode(uri)
+ log.info('Workspace inited: ', uri)
+ local logPath = ROOT / 'log' / (uri:gsub('[/:]+', '_') .. '.log')
+ log.info('Log path: ', logPath)
+ log.init(ROOT, logPath)
+end
+
+--- 创建排除文件匹配器
+function m.getIgnoreMatcher()
+ if m.ignoreVersion == config.version then
+ return m.ignoreMatcher
+ end
+
+ local pattern = {}
+ -- config.workspace.ignoreDir
+ for path in pairs(config.config.workspace.ignoreDir) do
+ log.info('Ignore directory:', path)
+ pattern[#pattern+1] = path
+ end
+ -- config.files.exclude
+ for path, ignore in pairs(config.other.exclude) do
+ if ignore then
+ log.info('Ignore by exclude:', path)
+ pattern[#pattern+1] = path
+ end
+ end
+ -- config.workspace.ignoreSubmodules
+ if config.config.workspace.ignoreSubmodules then
+ local buf = pub.awaitTask('loadFile', furi.encode(m.path .. '/.gitmodules'))
+ if buf then
+ for path in buf:gmatch('path = ([^\r\n]+)') do
+ log.info('Ignore by .gitmodules:', path)
+ pattern[#pattern+1] = path
+ end
+ end
+ end
+ -- config.workspace.useGitIgnore
+ if config.config.workspace.useGitIgnore then
+ local buf = pub.awaitTask('loadFile', furi.encode(m.path .. '/.gitignore'))
+ if buf then
+ for line in buf:gmatch '[^\r\n]+' do
+ log.info('Ignore by .gitignore:', line)
+ pattern[#pattern+1] = line
+ end
+ end
+ end
+ -- config.workspace.library
+ for path in pairs(config.config.workspace.library) do
+ log.info('Ignore by library:', path)
+ pattern[#pattern+1] = path
+ end
+
+ m.ignoreMatcher = glob.gitignore(pattern)
+
+ if platform.OS == "Windows" then
+ m.ignoreMatcher:setOption 'ignoreCase'
+ end
+
+ m.ignoreVersion = config.version
+ return m.ignoreMatcher
+end
+
+--- 文件是否被忽略
+function m.isIgnored(uri)
+ local path = furi.decode(uri)
+ local ignore = m.getIgnoreMatcher()
+ return ignore(path)
+end
+
+--- 预读工作区内所有文件
+function m.awaitPreload()
+ if not m.uri then
+ return
+ end
+ local max = 0
+ local read = 0
+ log.info('Preload start.')
+ local ignore = m.getIgnoreMatcher()
+
+ ignore:setInterface('type', function (path)
+ if fs.is_directory(fs.path(m.path .. '/' .. path)) then
+ return 'directory'
+ else
+ return 'file'
+ end
+ end)
+
+ ignore:setInterface('list', function (path)
+ local paths = {}
+ for fullpath in fs.path(m.path .. '/' .. path):list_directory() do
+ paths[#paths+1] = fullpath:string()
+ end
+ return paths
+ end)
+
+ ignore:scan(function (path)
+ local uri = furi.encode(m.path .. '/' .. path)
+ if not files.isLua(uri) then
+ return
+ end
+ max = max + 1
+ pub.task('loadFile', uri, function (text)
+ read = read + 1
+ --log.info(('Preload file at: %s , size = %.3f KB'):format(uri, #text / 1000.0))
+ files.setText(uri, text)
+ end)
+ end)
+
+ log.info(('Found %d files.'):format(max))
+ while true do
+ log.info(('Loaded %d/%d files'):format(read, max))
+ if read >= max then
+ break
+ end
+ await.sleep(0.1)
+ end
+
+ log.info('Preload finish.')
+ diagnostic.start()
+end
+
+--- 查找符合指定file path的所有uri
+---@param path string
+---@param whole boolean
+function m.findUrisByFilePath(path, whole)
+ local results = {}
+ for uri in files.eachFile() do
+ local pathLen = #path
+ local uriLen = #uri
+ if whole then
+ local seg = uri:sub(uriLen - pathLen, uriLen - pathLen)
+ if seg == '/' or seg == '\\' or seg == '' then
+ local see = uri:sub(uriLen - pathLen + 1, uriLen)
+ if files.eq(see, path) then
+ results[#results+1] = uri
+ end
+ end
+ else
+ for i = uriLen, uriLen - pathLen + 1, -1 do
+ local see = uri:sub(i - pathLen + 1, i)
+ if files.eq(see, path) then
+ results[#results+1] = uri
+ end
+ end
+ end
+ end
+ return results
+end
+
+--- 查找符合指定require path的所有uri
+---@param path string
+---@param whole boolean
+function m.findUrisByRequirePath(path, whole)
+ local results = {}
+ local mark = {}
+ local input = path:gsub('%.', '/')
+ for _, luapath in ipairs(config.config.runtime.path) do
+ local part = luapath:gsub('%?', input)
+ local uris = m.findUrisByFilePath(part, whole)
+ for _, uri in ipairs(uris) do
+ if not mark[uri] then
+ mark[uri] = true
+ results[#results+1] = uri
+ end
+ end
+ end
+ return results
+end
+
+function m.getRelativePath(uri)
+ local path = furi.decode(uri)
+ return fs.relative(fs.path(path), fs.path(m.path)):string()
+end
+
+return m