From f0212d54cf56f6d0b2582f02ebf612afd3bc5c06 Mon Sep 17 00:00:00 2001 From: notify Date: Fri, 21 Apr 2023 04:29:52 +0800 Subject: [PATCH] Pattern (#129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为Pattern增加了否定语法 优化几个类的__tostring元方法 增加调试器第三方库,编写调试文档 --- docs/diy/{03-events.rst => 07-events.rst} | 0 docs/diy/08-debugging.rst | 75 +++ docs/diy/09-exppattern.rst | 10 + docs/diy/index.rst | 4 +- lua/core/card.lua | 2 +- lua/core/debug.lua | 18 +- lua/core/exppattern.lua | 290 +++++++---- lua/core/player.lua | 2 +- lua/core/skill.lua | 4 + lua/lib/debugger.lua | 590 ++++++++++++++++++++++ test.lua | 7 + 11 files changed, 888 insertions(+), 114 deletions(-) rename docs/diy/{03-events.rst => 07-events.rst} (100%) create mode 100644 docs/diy/08-debugging.rst create mode 100644 docs/diy/09-exppattern.rst create mode 100644 lua/lib/debugger.lua create mode 100644 test.lua diff --git a/docs/diy/03-events.rst b/docs/diy/07-events.rst similarity index 100% rename from docs/diy/03-events.rst rename to docs/diy/07-events.rst diff --git a/docs/diy/08-debugging.rst b/docs/diy/08-debugging.rst new file mode 100644 index 00000000..3fd47937 --- /dev/null +++ b/docs/diy/08-debugging.rst @@ -0,0 +1,75 @@ +.. SPDX-License-Identifier: GFDL-1.3-or-later + +技巧:调试您的代码 +================== + +Lua是一门很不错的语言。然而,其在调试上却稍有困难。网上或许能找到一些针对独立运行的Lua脚本的调试器(例如vscode能下载的到的各种Lua debugger),但却不适用于FK。 + +因此,本文将试图为你扫清Lua调试方面的障碍。 + +假设你正使用Windows系统,那么启动FK的时候,应该是一个游戏窗口+一个黑色的命令行窗口。调试工作基本就是在黑色窗口进行的。 + +使用debugger.lua +---------------- + +这应该是最为推荐的一种做法了,FreeKill在lib中引用了debugger.lua作为调试库。 + +以下只简要介绍一下用法,最详细的详情请去项目官网查看: https://github.com/slembcke/debugger.lua + +当你想要在代码中下断点时,就调用 ``dbg()`` 函数。当执行到这里时,就会停下来并在命令行中显示类似gdb的界面。 + +例如: + +.. code:: lua + + local room = player.room + + dbg() -- 相当于下了断点,后面就可以来此进行调试 + player:drawCards(1) + +上面的代码中就调用了debugger.lua,让程序进行了中断,然后命令行就进入了调试界面。 + +.. hint:: + + 在默认的双击启用exe带有的命令行中,颜色可能会显示的非常奇怪。 + + 如果你遇到了颜色不能正常显示的问题,推荐你使用Git Bash或者Windows Terminal之类的终端模拟器,然后在命令行中通过FreeKill.exe来启动游戏。 + +下面来说说调试的基本用法:使用 ``h`` 命令显示帮助信息。debugger.lua已经被我中文化了。 + +.. tip:: + + 其实也可以用lua自带的 ``debug.debug()`` 进行交互式调试,不过功能比debugger.lua弱得多了。 + +.. warning:: + + 在Linux上使用FreeKill -s开服时不能用这个调试器!因为stdin已经被服务端shell占用了,所以无法调试。 + +一些在调试中可能有用的函数 +-------------------------- + +在调试器中直接输入Lua语句就能执行。以下是一些可能用得到的函数: + +print +~~~~~ + +遇事不决print,这是当时没有调试器可用时候的措施。简单但却实用。 + +现在可以直接用debugger.lua的 p 命令输出表达式的值了,无需再自己写一堆。 + +p +~~~ + +``p`` 也是个函数,是inspect库的包装。它能详细输出表中的所有值,包括元表。 + +因此在使用它输出和类相关的东西的时候还是放弃为好... + +json.encode +~~~~~~~~~~~ + +将不含循环引用的表转换为json字符串。或许会很有用吧。但是不如p就是了。 + +GameLogic:dumpEventStack() +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +输出当前的事件栈。在处理插结的时候能派上用场。 diff --git a/docs/diy/09-exppattern.rst b/docs/diy/09-exppattern.rst new file mode 100644 index 00000000..ae2a3044 --- /dev/null +++ b/docs/diy/09-exppattern.rst @@ -0,0 +1,10 @@ +.. SPDX-License-Identifier: GFDL-1.3-or-later + +解析:Exppattern +================ + +所谓Exppattern,类似于各大编程语言中的正则表达式。不过和正则表达式不同的是,正则匹配的是字符串,而Exppattern匹配的对象是一张张卡牌。通过Exppattern可以判断各种卡牌的情况,比如这张牌是否符合“既是红桃,也是点数3-5的牌”等等一系列复杂的规则。 + +你可能也已经注意到了,在不少Room中的askFor...函数中,出现了很多次pattern参数。这个pattern就是Exppattern,它用来辅助确定询问的卡牌必须满足哪些需求。 + + diff --git a/docs/diy/index.rst b/docs/diy/index.rst index ff87a7bd..9b7d001e 100644 --- a/docs/diy/index.rst +++ b/docs/diy/index.rst @@ -12,4 +12,6 @@ Diy文档 04-newskill.rst 05-trigger.rst 06-active.rst - 03-events.rst + 07-events.rst + 08-debugging.rst + 09-exppattern.rst diff --git a/lua/core/card.lua b/lua/core/card.lua index 45ab7f3b..edcfee53 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -121,7 +121,7 @@ function Card:initialize(name, suit, number, color) end function Card:__tostring() - return string.format("%s[%s %d]", self.name, self:getSuitString(), self.number) + return string.format("", self.name, self:getSuitString(), self.number) end --- 克隆特定卡牌并赋予花色与点数。 diff --git a/lua/core/debug.lua b/lua/core/debug.lua index bd24e462..423aa5f2 100644 --- a/lua/core/debug.lua +++ b/lua/core/debug.lua @@ -2,21 +2,7 @@ ---@diagnostic disable: lowercase-global inspect = require "inspect" - -DebugMode = true -function PrintWhenMethodCall() - local info = debug.getinfo(2) - local name = info.name - local line = info.currentline - local namewhat = info.namewhat - local shortsrc = info.short_src - if (namewhat == "method") and - (shortsrc ~= "[C]") and - (not string.find(shortsrc, "/lib")) then - print(shortsrc .. ":" .. line .. ": " .. name) - end -end ---debug.sethook(PrintWhenMethodCall, "c") +dbg = require "debugger" function p(v) print(inspect(v)) end -function pt(t) for k,v in pairs(t)do print(k,v) end end +function pt(t) for k, v in pairs(t) do print(k, v) end end diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua index 4a53b0a5..83ff0749 100644 --- a/lua/core/exppattern.lua +++ b/lua/core/exppattern.lua @@ -16,14 +16,19 @@ 例如: slash,jink|2~4|spade;.|.|.|.|.|trick + 你可以使用 '^' 符号表示否定,比如 ^heart 表示除了红桃之外的所有花色。 + 否定型一样的可以与其他表达式并用,用 ',' 分割。 + 如果要同时否定多项,则需要用括号: ^(heart, club) 等。 + 注:这种括号不支持嵌套否定。 + ]]-- ---@class Matcher ----@field public name string[] +---@field public trueName string[] ---@field public number integer[] ---@field public suit string[] ---@field public place string[] ----@field public generalName string[] +---@field public name string[] ---@field public cardType string[] ---@field public id integer[] @@ -34,23 +39,44 @@ local numbertable = { ["K"] = 13, } -local suittable = { - [Card.Spade] = "spade", - [Card.Club] = "club", - [Card.Heart] = "heart", - [Card.Diamond] = "diamond", -} - local placetable = { [Card.PlayerHand] = "hand", [Card.PlayerEquip] = "equip", } -local typetable = { - [Card.TypeBasic] = "basic", - [Card.TypeTrick] = "trick", - [Card.TypeEquip] = "equip", -} +local function matchSingleKey(matcher, card, key) + local match = matcher[key] + if not match then return true end + local val = card[key] + if key == "suit" then + val = card:getSuitString() + elseif key == "cardType" then + val = card:getTypeString() + elseif key == "place" then + val = placetable[Fk:currentRoom():getCardArea(card.id)] + if not val then + for _, p in ipairs(Fk:currentRoom().alive_players) do + val = p:getPileNameOfId(card.id) + if val then break end + end + end + end + + if table.contains(match, val) then + return true + else + local neg = match.neg + if not neg then return false end + for _, t in ipairs(neg) do + if type(t) == "table" then + if not table.contains(t, val) then return true end + else + if t ~= val then return true end + end + end + end + return false +end ---@param matcher Matcher ---@param card Card @@ -59,50 +85,13 @@ local function matchCard(matcher, card) card = Fk:getCardById(card) end - if matcher.name and not table.contains(matcher.name, card.name) and - not table.contains(matcher.name, card.trueName) then - return false - end - - if matcher.number and not table.contains(matcher.number, card.number) then - return false - end - - if matcher.suit and not table.contains(matcher.suit, card:getSuitString()) then - return false - end - - if matcher.place and not table.contains( - matcher.place, - placetable[Fk:currentRoom():getCardArea(card.id)] - ) then - local piles = table.filter(matcher.place, function(e) - return not table.contains(placetable, e) - end) - for _, pi in ipairs(piles) do - if ClientInstance then - if Self:getPileNameOfId(card.id) == pi then return true end - else - for _, p in ipairs(RoomInstance.alive_players) do - local pile = p:getPileNameOfId(card.id) - if pile == pi then return true end - end - end - end - return false - end - - -- TODO: generalName - - if matcher.cardType and not table.contains(matcher.cardType, typetable[card.type]) then - return false - end - - if matcher.id and not table.contains(matcher.id, card.id) then - return false - end - - return true + return matchSingleKey(matcher, card, "trueName") + and matchSingleKey(matcher, card, "number") + and matchSingleKey(matcher, card, "suit") + and matchSingleKey(matcher, card, "place") + and matchSingleKey(matcher, card, "name") + and matchSingleKey(matcher, card, "cardType") + and matchSingleKey(matcher, card, "id") end local function hasIntersection(a, b) @@ -119,6 +108,9 @@ local function hasIntersection(a, b) return true end end + + -- TODO: 判断含有neg的两个matcher + return false end @@ -126,11 +118,11 @@ end ---@param b Matcher local function matchMatcher(a, b) local keys = { - "name", + "trueName", "number", "suit", "place", - "generalName", + "name", "cardType", "id", } @@ -144,6 +136,104 @@ local function matchMatcher(a, b) return true end +local function parseNegative(list) + local bracket = nil + local toRemove = {} + for i, element in ipairs(list) do + if element[1] == "^" or bracket then + list.neg = list.neg or {} + table.insert(toRemove, 1, i) + if element[1] == "^" and element[2] == "(" then + if bracket then + error("pattern syntax error. Cannot use nested bracket.") + else + bracket = {} + end + element = element:sub(3) + else + if element[1] == "^" then + element = element:sub(2) + end + end + + local eofBracket + if element:endsWith(")") then + eofBracket = true + element = element:sub(1, -2) + end + + if eofBracket then + if not bracket then + error('pattern syntax error. No matching bracket.') + else + table.insert(bracket, element) + table.insert(list.neg, bracket) + bracket = nil + end + else + if bracket then + table.insert(bracket, element) + else + table.insert(list.neg, element) + end + end + end + end + + for _, i in ipairs(toRemove) do + table.remove(list, i) + end +end + +local function parseNumToTable(from, dest) + for _, num in ipairs(from) do + if type(num) ~= "string" then goto continue end + local n = tonumber(num) + if not n then + n = numbertable[num] + end + if n then + table.insertIfNeed(dest, n) + else + if string.find(num, "~") then + local s, e = table.unpack(num:split("~")) + local start = tonumber(s) + if not start then + start = numbertable[s] + end + local _end = tonumber(e) + if not _end then + _end = numbertable[e] + end + + for i = start, _end do + table.insertIfNeed(dest, i) + end + end + end + ::continue:: + end +end + +local function parseRawNumTable(tab) + local ret = {} + parseNumToTable(tab, ret) + + if tab.neg then + ret.neg = {} + parseNumToTable(tab.neg, ret.neg) + + for _, t in ipairs(tab.neg) do + if type(t) == "table" then + local tmp = {} + parseNumToTable(t, tmp) + table.insert(ret.neg, tmp) + end + end + end + return ret +end + local function parseMatcher(str) local t = str:split("|") if #t < 7 then @@ -156,56 +246,57 @@ local function parseMatcher(str) t[i] = item:split(",") end + for _, list in ipairs(t) do + parseNegative(list) + end + local ret = {} ---@type Matcher - ret.name = not table.contains(t[1], ".") and t[1] or nil + ret.trueName = not table.contains(t[1], ".") and t[1] or nil if not table.contains(t[2], ".") then - ret.number = {} - for _, num in ipairs(t[2]) do - local n = tonumber(num) - if not n then - n = numbertable[num] - end - if n then - table.insertIfNeed(ret.number, n) - else - if string.find(num, "~") then - local s, e = table.unpack(num:split("~")) - local start = tonumber(s) - if not start then - start = numbertable[s] - end - local _end = tonumber(e) - if not _end then - _end = numbertable[e] - end - - for i = start, _end do - table.insertIfNeed(ret.number, i) - end - end - end - end + ret.number = parseRawNumTable(t[2]) end ret.suit = not table.contains(t[3], ".") and t[3] or nil ret.place = not table.contains(t[4], ".") and t[4] or nil - ret.generalName = not table.contains(t[5], ".") and t[5] or nil + ret.name = not table.contains(t[5], ".") and t[5] or nil ret.cardType = not table.contains(t[6], ".") and t[6] or nil if not table.contains(t[7], ".") then - ret.id = {} - for _, num in ipairs(t[6]) do - local n = tonumber(num) - if n and n > 0 then - table.insertIfNeed(ret.id, n) - end - end + ret.id = parseRawNumTable(t[7]) end return ret end +local function matcherKeyToString(tab) + if not tab then return "." end + local ret = table.concat(tab, ",") + if tab.neg then + for _, t in ipairs(tab.neg) do + if ret ~= "" then ret = ret .. "," end + if type(t) == "table" then + ret = ret .. ("^(" .. table.concat(t, ",") .. ")") + else + ret = ret .. "^" .. t + end + end + end + return ret +end + +local function matcherToString(matcher) + return table.concat({ + matcherKeyToString(matcher.trueName), + matcherKeyToString(matcher.number), + matcherKeyToString(matcher.suit), + matcherKeyToString(matcher.place), + matcherKeyToString(matcher.name), + matcherKeyToString(matcher.cardType), + matcherKeyToString(matcher.id), + }, "|") +end + ---@class Exppattern: Object ---@field public matchers Matcher[] local Exppattern = class("Exppattern") @@ -266,4 +357,13 @@ function Exppattern:matchExp(exp) return false end +function Exppattern:__tostring() + local ret = "" + for i, matcher in ipairs(self.matchers) do + if i > 1 then ret = ret .. ";" end + ret = ret .. matcherToString(matcher) + end + return ret +end + return Exppattern diff --git a/lua/core/player.lua b/lua/core/player.lua index 3ac086e8..010ff50b 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -99,7 +99,7 @@ function Player:initialize() end function Player:__tostring() - return string.format("%s #%d", self.id < 0 and "Bot" or "Player", math.abs(self.id)) + return string.format("<%s %d>", self.id < 0 and "Bot" or "Player", math.abs(self.id)) end --- 设置角色、体力、技能。 diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 5e9c8e12..6cf723d7 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -53,6 +53,10 @@ function Skill:initialize(name, frequency) self.attached_equip = nil end +function Skill:__tostring() + return "" +end + --- 为一个技能增加相关技能。 ---@param skill Skill @ 技能 function Skill:addRelatedSkill(skill) diff --git a/lua/lib/debugger.lua b/lua/lib/debugger.lua new file mode 100644 index 00000000..53c296f8 --- /dev/null +++ b/lua/lib/debugger.lua @@ -0,0 +1,590 @@ +--[[ + Copyright (c) 2023 Scott Lembcke and Howling Moon Software + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + TODO: + * Print short function arguments as part of stack location. + * Properly handle being reentrant due to coroutines. +]] + +-- notify 汉化 并根据fk/lua 5.4实际情况魔改 + +local dbg + +-- ** FreeKill **: local the deleted global var here +local io = io +local os = os +local load = load + +-- Use ANSI color codes in the prompt by default. +local COLOR_GRAY = "" +local COLOR_RED = "" +local COLOR_BLUE = "" +local COLOR_YELLOW = "" +local COLOR_RESET = "" +local GREEN_CARET = " => " + +local function pretty(obj, max_depth) + if max_depth == nil then max_depth = dbg.pretty_depth end + + -- Returns true if a table has a __tostring metamethod. + local function coerceable(tbl) + local meta = getmetatable(tbl) + return (meta and meta.__tostring) + end + + local function recurse(obj, depth) + if type(obj) == "string" then + -- Dump the string so that escape sequences are printed. + return string.format("%q", obj) + elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then + local str = "{" + + for k, v in pairs(obj) do + local pair = pretty(k, 0).." = "..recurse(v, depth + 1) + str = str..(str == "{" and pair or ", "..pair) + end + + return str.."}" + else + -- tostring() can fail if there is an error in a __tostring metamethod. + local success, value = pcall(function() return tostring(obj) end) + return (success and value or "") + end + end + + return recurse(obj, 0) +end + +-- The stack level that cmd_* functions use to access locals or info +-- The structure of the code very carefully ensures this. +local CMD_STACK_LEVEL = 6 + +-- Location of the top of the stack outside of the debugger. +-- Adjusted by some debugger entrypoints. +local stack_top = 0 + +-- The current stack frame index. +-- Changed using the up/down commands +local stack_inspect_offset = 0 + +-- LuaJIT has an off by one bug when setting local variables. +local LUA_JIT_SETLOCAL_WORKAROUND = 0 + +-- Default dbg.read function +local function dbg_read(prompt) + dbg.write(prompt) + io.flush() + return io.read() +end + +-- Default dbg.write function +local function dbg_write(str) + io.write(str) +end + +local function dbg_writeln(str, ...) + if select("#", ...) == 0 then + dbg.write((str or "").."\n") + else + dbg.write(string.format(str.."\n", ...)) + end +end + +local function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..":"..COLOR_YELLOW..line..COLOR_RESET end +local function format_stack_frame_info(info) + local filename = info.source:match("@(.*)") + local source = filename and dbg.shorten_path(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = (info.name and "'"..COLOR_BLUE..info.name..COLOR_RESET.."'" or format_loc(source, info.linedefined)) + return format_loc(source, info.currentline)..", 在"..namewhat.." "..name +end + +local repl + +-- Return false for stack frames without source, +-- which includes C frames, Lua bytecode, and `loadstring` functions +local function frame_has_line(info) return info.currentline >= 0 end + +local function hook_factory(repl_threshold) + return function(offset, reason) + return function(event, _) + -- Skip events that don't have line information. + if not frame_has_line(debug.getinfo(2)) then return end + + -- Tail calls are specifically ignored since they also will have tail returns to balance out. + if event == "call" then + offset = offset + 1 + elseif event == "return" and offset > repl_threshold then + offset = offset - 1 + elseif event == "line" and offset <= repl_threshold then + repl(reason) + end + end + end +end + +local hook_step = hook_factory(1) +local hook_next = hook_factory(0) +local hook_finish = hook_factory(-1) + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command, but are when running the print command. +local function local_bindings(offset, include_globals) + local level = offset + stack_inspect_offset + CMD_STACK_LEVEL + local func = debug.getinfo(level).func + local bindings = {} + + -- Retrieve the upvalues + do local i = 1; while true do + local name, value = debug.getupvalue(func, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the locals (overwriting any upvalues) + do local i = 1; while true do + local name, value = debug.getlocal(level, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do local i = 1; while true do + local name, value = debug.getlocal(level, -i) + if not name then break end + varargs[i] = value + i = i + 1 + end end + if #varargs > 0 then bindings["..."] = varargs end + + if include_globals then + -- In Lua 5.2, you have to get the environment table from the function's locals. + local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV) + return setmetatable(bindings, {__index = env or _G}) + else + return bindings + end +end + +-- Used as a __newindex metamethod to modify variables in cmd_eval(). +local function mutate_bindings(_, name, value) + local FUNC_STACK_OFFSET = 3 -- Stack depth of this function. + local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL + + -- Set a local. + do local i = 1; repeat + local var = debug.getlocal(level, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了局部变量 "..COLOR_BLUE..name..COLOR_RESET) + return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value) + end + i = i + 1 + until var == nil end + + -- Set an upvalue. + local func = debug.getinfo(level).func + do local i = 1; repeat + local var = debug.getupvalue(func, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了上值 "..COLOR_BLUE..name..COLOR_RESET) + return debug.setupvalue(func, i, value) + end + i = i + 1 + until var == nil end + + -- Set a global. + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了全局变量 "..COLOR_BLUE..name..COLOR_RESET) + _G[name] = value +end + +-- Compile an expression with the given variable bindings. +local function compile_chunk(block, env) + local source = "debugger.lua REPL" + local chunk = nil + + if _VERSION <= "Lua 5.1" then + chunk = loadstring(block, source) + if chunk then setfenv(chunk, env) end + else + -- The Lua 5.2 way is a bit cleaner + chunk = load(block, source, "t", env) + end + + if not chunk then dbg_writeln(COLOR_RED.."错误: 无法编译代码:\n"..COLOR_RESET..block) end + return chunk +end + +local SOURCE_CACHE = {} + +local function where(info, context_lines) + local source = SOURCE_CACHE[info.source] + if not source then + source = {} + local filename = info.source:match("@(.*)") + if filename then + pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) + elseif info.source then + for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end + end + SOURCE_CACHE[info.source] = source + end + + if source and source[info.currentline] then + for i = info.currentline - context_lines, info.currentline + context_lines do + local tab_or_caret = (i == info.currentline and GREEN_CARET or " ") + local line = source[i] + if line then dbg_writeln(COLOR_GRAY.."% 4d"..tab_or_caret.."%s", i, line) end + end + else + dbg_writeln(COLOR_RED.."错误: 源码不可用: "..COLOR_BLUE..info.short_src); + end + + return false +end + +-- Wee version differences +local unpack = unpack or table.unpack +local pack = function(...) return {n = select("#", ...), ...} end + +local function cmd_step() + stack_inspect_offset = stack_top + return true, hook_step +end + +local function cmd_next() + stack_inspect_offset = stack_top + return true, hook_next +end + +local function cmd_finish() + local offset = stack_top - stack_inspect_offset + stack_inspect_offset = stack_top + return true, offset < 0 and hook_factory(offset - 1) or hook_finish +end + +local function cmd_print(expr) + local env = local_bindings(1, true) + local chunk = compile_chunk("return "..expr, env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local results = pack(pcall(chunk, unpack(rawget(env, "...") or {}))) + + -- The first result is the pcall error. + if not results[1] then + dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." "..results[2]) + else + local output = "" + for i = 2, results.n do + output = output..(i ~= 2 and ", " or "")..dbg.pretty(results[i]) + end + + if output == "" then output = "<无返回值>" end + dbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output) + end + + return false +end + +local function cmd_eval(code) + local env = local_bindings(1, true) + local mutable_env = setmetatable({}, { + __index = env, + __newindex = mutate_bindings, + }) + + local chunk = compile_chunk(code, mutable_env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local success, err = pcall(chunk, unpack(rawget(env, "...") or {})) + if not success then + dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." "..tostring(err)) + end + + return false +end + +local function cmd_down() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset + 1 + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until not info or frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("目前所在的栈帧: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("已经位于栈底。") + end + + return false +end + +local function cmd_up() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset - 1 + if offset < stack_top then info = nil; break end + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("目前所在的栈帧: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("已经位于栈顶。") + end + + return false +end + +local function cmd_where(context_lines) + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + return (info and where(info, tonumber(context_lines) or 5)) +end + +local function cmd_trace() + dbg_writeln("目前在栈帧 %d", stack_inspect_offset - stack_top) + local i = 0; while true do + local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i) + if not info then break end + + local is_current_frame = (i + stack_top == stack_inspect_offset) + local tab_or_caret = (is_current_frame and GREEN_CARET or " ") + dbg_writeln(COLOR_GRAY.."% 4d"..COLOR_RESET..tab_or_caret.."%s", i, format_stack_frame_info(info)) + i = i + 1 + end + + return false +end + +local function cmd_locals() + local bindings = local_bindings(1, false) + + -- Get all the variable binding names and sort them + local keys = {} + for k, _ in pairs(bindings) do table.insert(keys, k) end + table.sort(keys) + + for _, k in ipairs(keys) do + local v = bindings[k] + + -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object. + if not rawequal(v, dbg) and k ~= "_ENV" and not k:match("%(.*%)") then + dbg_writeln(" "..COLOR_BLUE..k.. GREEN_CARET..dbg.pretty(v)) + end + end + + return false +end + +local function cmd_help() + dbg.write("" + ..COLOR_BLUE.." <回车>"..GREEN_CARET.."重复执行上一条命令\n" + ..COLOR_BLUE.." c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."继续执行代码\n" + ..COLOR_BLUE.." s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."单步执行下一行 (会深入到函数中)\n" + ..COLOR_BLUE.." n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."单步执行下一行 (不深入到函数调用)\n" + ..COLOR_BLUE.." f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."一直执行直到此函数返回\n" + ..COLOR_BLUE.." u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."上移一个栈帧\n" + ..COLOR_BLUE.." d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."下移一个栈帧\n" + ..COLOR_BLUE.." w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[行数]"..GREEN_CARET.."打印出当前行周围的代码\n" + ..COLOR_BLUE.." e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[语句]"..GREEN_CARET.."执行一个语句\n" + ..COLOR_BLUE.." p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[表达式]"..GREEN_CARET.."求出表达式的值,并打印出结果\n" + ..COLOR_BLUE.." t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."打印函数调用栈\n" + ..COLOR_BLUE.." l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."打印函数参数、局部变量和上值\n" + ..COLOR_BLUE.." h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."打印这条消息\n" + -- ..COLOR_BLUE.." q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."结束调试,继续执行代码\n" + ) + return false +end + +local last_cmd = false + +local commands = { + ["^c$"] = function() return true end, + ["^s$"] = cmd_step, + ["^n$"] = cmd_next, + ["^f$"] = cmd_finish, + ["^p%s+(.*)$"] = cmd_print, + ["^e%s+(.*)$"] = cmd_eval, + ["^u$"] = cmd_up, + ["^d$"] = cmd_down, + ["^w%s*(%d*)$"] = cmd_where, + ["^t$"] = cmd_trace, + ["^l$"] = cmd_locals, + ["^h$"] = cmd_help, + ["^q$"] = function() dbg.exit(0); return true end, +} + +local function match_command(line) + for pat, func in pairs(commands) do + -- Return the matching command and capture argument. + if line:find(pat) then return func, line:match(pat) end + end +end + +-- Run a command line +-- Returns true if the REPL should exit and the hook function factory +local function run_command(line) + -- GDB/LLDB exit on ctrl-d + if line == nil then dbg.exit(1); return true end + + -- Re-execute the last command if you press return. + if line == "" then line = last_cmd or "h" end + + local command, command_arg = match_command(line) + if command then + last_cmd = line + -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable. + return unpack({command(command_arg)}) + elseif dbg.auto_eval then + return unpack({cmd_eval(line)}) + else + dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." 无法识别命令 '%s'。\n输入 'h' 并按下回车键来查看命令列表。", line) + return false + end +end + +repl = function(reason) + -- Skip frames without source info. + while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do + stack_inspect_offset = stack_inspect_offset + 1 + end + + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3) + reason = reason and (COLOR_YELLOW.."由于 "..COLOR_RED..reason..GREEN_CARET.." 中断执行\n") or "" + dbg_writeln(reason..format_stack_frame_info(info)) + + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + + repeat + local success, done, hook = pcall(run_command, dbg.read(COLOR_RED.."(dbg) "..COLOR_RESET)) + if success then + debug.sethook(hook and hook(0), "crl") + else + local message = COLOR_RED.."INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:"..COLOR_RESET.." "..done + dbg_writeln(message) + error(message) + end + until done +end + +-- Make the debugger object callable like a function. +dbg = setmetatable({}, { + __call = function(_, condition, top_offset, source) + if condition then return end + + top_offset = (top_offset or 0) + stack_inspect_offset = top_offset + stack_top = top_offset + + debug.sethook(hook_next(1, source or "dbg()"), "crl") + return + end, +}) + +-- Expose the debugger's IO functions. +dbg.read = dbg_read +dbg.write = dbg_write +dbg.shorten_path = function (path) return path end +dbg.exit = function() end + +dbg.writeln = dbg_writeln + +dbg.pretty_depth = 3 +dbg.pretty = pretty +dbg.pp = function(value, depth) dbg_writeln(dbg.pretty(value, depth)) end + +dbg.auto_where = 1 +dbg.auto_eval = true + +local lua_error, lua_assert = error, assert + +-- Works like error(), but invokes the debugger. +function dbg.error(err, level) + level = level or 1 + dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(err)) + dbg(false, level, "dbg.error()") + + lua_error(err, level) +end + +-- Works like assert(), but invokes the debugger on a failure. +function dbg.assert(condition, message) + if not condition then + dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET..message) + dbg(false, 1, "dbg.assert()") + end + + return lua_assert(condition, message) +end + +-- Works like pcall(), but invokes the debugger on an error. +function dbg.call(f, ...) + return xpcall(f, function(err) + dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(err)) + dbg(false, 1, "dbg.call()") + + return err + end, ...) +end + +-- Error message handler that can be used with lua_pcall(). +function dbg.msgh(...) + if debug.getinfo(2) then + dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(...)) + dbg(false, 1, "dbg.msgh()") + else + dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Lua代码中未发生错误。将在 dbg_pcall() 完成后继续执行代码。") + end + + return ... +end + +-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them. +local stdin_isatty = true +local stdout_isatty = true + +-- Conditionally enable color support. +local color_maybe_supported = (stdout_isatty and os.getenv("TERM") and os.getenv("TERM") ~= "dumb") +if color_maybe_supported and not os.getenv("DBG_NOCOLOR") then + COLOR_GRAY = string.char(27) .. "[90m" + COLOR_RED = string.char(27) .. "[91m" + COLOR_BLUE = string.char(27) .. "[94m" + COLOR_YELLOW = string.char(27) .. "[33m" + COLOR_RESET = string.char(27) .. "[0m" + GREEN_CARET = string.char(27) .. "[92m => "..COLOR_RESET +end + +return dbg diff --git a/test.lua b/test.lua new file mode 100644 index 00000000..6cc9298d --- /dev/null +++ b/test.lua @@ -0,0 +1,7 @@ +fk={}pcall(function()dofile'lua/freekill.lua'end) +a=Exppattern:Parse('slash,asd,df,^sd,sfff,^(xzc,afsd)|34,23|.|.|.|.|^(23~32)') +p(a.matchers) +c = { trueName = 'slash', number = 4, id = 155 } +p(a:match(c)) +p(a:matchExp('^jink')) +print(a)