(* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
                         -****************+.
               .-=++*+::*###############*-
           :=+##*+=-::*##**############=
        .=*#*=:    .+#*=+############+.
       =##+.      +#+-=############*-:
     :*#+.      =+-.=#############- =#*.
    -##-      -=..+#############*:.  ---.
   -##:     .: .+################= :+#*-:.
  .##-        :==========*#####= :*#*- -#*
  =##                   -####*:-*#*-   .##:
  +#+                  -###*--*#*-      *#=
  *#+                 +###==*#*:        *#=
  +#*                *##+=*#*-          ##-
  :##.             .*##*##*:           -##.
   +#*            -#####*:             *#=
    *#+          =####+:             .*#+
     +#*.       +###+.              :##=
      -##+.    *##+:              :+#*:
        =*#+..*#+:             .-*#*-
          :-:#+::.        .:-=*##+:
           -+..+*###****####*+-.
          :.      ..:::::..
        ____                   _
       / ___| _ __   __ _ _ __| | __
       \___ \| '_ \ / _` | '__| |/ /
        ___) | |_) | (_| | |  |   <
       |____/| .__/ \__,_|_|  |_|\_\
             |_|   Game Toolkit™

Copyright © 2024-present tinyBigGAMES™ LLC
         All Rights Reserved.

Website: https://tinybiggames.com
Email  : support@tinybiggames.com

See LICENSE for license information
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *)

unit SGT.LuaDebugger;

{$I SGT.defines.inc}

interface

uses
  System.SysUtils,
  System.AnsiStrings,
  SGT.Deps,
  SGT.Deps.Ext;

// This function must be called before calling dbg_pcall() to set up the debugger module.
// 'name' must be the name of the module to register the debugger as. (to use with require 'module')
// 'globalName' can either be NULL or a global variable name to assign the debugger to. (I use "dbg")
// 'readFunc' is a lua_CFunction that returns a line of input when called. Pass NULL if you want to read from stdin.
// 'writeFunc' is a lua_CFunction that takes a single string as an argument. Pass NULL if you want to write to stdout.
procedure dbg_setup(lua: Plua_State; name: PAnsiChar; globalName: PAnsiChar; readFunc: lua_CFunction; writeFunc: lua_CFunction); cdecl;

// Same as 'dbg_setup(lua, "debugger", "dbg", NULL, NULL)'
procedure dbg_setup_default(lua: Plua_State); cdecl;

// Drop in replacement for lua_pcall() that attaches the debugger on an error if 'msgh' is 0.
function  dbg_pcall(lua: Plua_State; nargs: Integer; nresults: Integer; msgh: Integer): Integer; cdecl;

// Drop in replacement for luaL_dofile()
function dbg_dofile(lua: Plua_State; filename: PAnsiChar): Integer; cdecl;

implementation

(*
  local COLOR_GRAY = "\27[90m"
  local COLOR_RED = "\27[91m"
  local COLOR_BLUE = "\27[94m"
  local COLOR_YELLOW = "\27[93m"
  local COLOR_RESET = "\27[0m"
  local GREEN_CARET = "\27[92m => \27[0m"
*)

const
  DEBUGGER_SRC: PAnsiChar =
  '''
  -- SPDX-License-Identifier: MIT
  -- Copyright (c) 2024 Scott Lembcke and Howling Moon Software

  local dbg

  -- Use ANSI color codes in the prompt by default.
  local COLOR_GRAY = "\27[90m"
  local COLOR_RED = "\27[91m"
  local COLOR_BLUE = "\27[94m"
  local COLOR_YELLOW = "\27[93m"
  local COLOR_RESET = "\27[0m"
  local GREEN_CARET = "\27[92m => \27[0m"

  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 "<!!error in __tostring metamethod!!>")
      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 "<NULL>").."\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).." in "..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.."Set local variable "..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.."Set upvalue "..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.."Set global variable "..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.."Error: Could not compile block:\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.."Error: Source not available for "..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.."Error:"..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 = "<no result>" 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.."Error:"..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("Inspecting frame: "..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("Already at the bottom of the stack.")
    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("Inspecting frame: "..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("Already at the top of the stack.")
    end
    
    return false
  end

  local function cmd_inspect(offset)
    offset = stack_top + tonumber(offset)
    local info = debug.getinfo(offset + CMD_STACK_LEVEL)
    if info then
      stack_inspect_offset = offset
      dbg.writeln("Inspecting frame: "..format_stack_frame_info(info))
    else
      dbg.writeln(COLOR_RED.."ERROR: "..COLOR_BLUE.."Invalid stack frame index."..COLOR_RESET)
    end
  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("Inspecting frame %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.."  <return>"..GREEN_CARET.."re-run last command\n"
      ..COLOR_BLUE.."  c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."continue execution\n"
      ..COLOR_BLUE.."  s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."step forward by one line (into functions)\n"
      ..COLOR_BLUE.."  n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."step forward by one line (skipping over functions)\n"
      ..COLOR_BLUE.."  f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."step forward until exiting the current function\n"
      ..COLOR_BLUE.."  u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."move up the stack by one frame\n"
      ..COLOR_BLUE.."  d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."move down the stack by one frame\n"
      ..COLOR_BLUE.."  i"..COLOR_YELLOW.."(nspect) "..COLOR_BLUE.."[index]"..GREEN_CARET.."move to a specific stack frame\n"
      ..COLOR_BLUE.."  w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[line count]"..GREEN_CARET.."print source code around the current line\n"
      ..COLOR_BLUE.."  e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[statement]"..GREEN_CARET.."execute the statement\n"
      ..COLOR_BLUE.."  p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[expression]"..GREEN_CARET.."execute the expression and print the result\n"
      ..COLOR_BLUE.."  t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."print the stack trace\n"
      ..COLOR_BLUE.."  l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."print the function arguments, locals and upvalues.\n"
      ..COLOR_BLUE.."  h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."print this message\n"
      ..COLOR_BLUE.."  q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."halt execution\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,
    ["i%s*(%d+)"] = cmd_inspect,
    ["^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.."Error:"..COLOR_RESET.." command '%s' not recognized.\nType 'h' and press return for a command list.", 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.."break via "..COLOR_RED..reason..GREEN_CARET) 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.."debugger.lua> "..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(err) os.exit(err) 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 = false
  dbg.auto_eval = false

  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.."ERROR: "..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)
    message = message or "assertion failed!"
    if not condition then
      dbg_writeln(COLOR_RED.."ERROR: "..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.."ERROR: "..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.."ERROR: "..COLOR_RESET..dbg.pretty(...))
      dbg(false, 1, "dbg.msgh()")
    else
      dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Error did not occur in Lua code. Execution will continue after 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 the LuaJIT FFI.
  local ffi = (jit and require("ffi"))
  if ffi then
    ffi.cdef[[
      int isatty(int); // Unix
      int _isatty(int); // Windows
      void free(void *ptr);
      
      char *readline(const char *);
      int add_history(const char *);
    ]]
    
    local function get_func_or_nil(sym)
      local success, func = pcall(function() return ffi.C[sym] end)
      return success and func or nil
    end
    
    local isatty = get_func_or_nil("isatty") or get_func_or_nil("_isatty") or (ffi.load("ucrtbase"))["_isatty"]
    stdin_isatty = isatty(0)
    stdout_isatty = isatty(1)
  end

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

  if stdin_isatty and not os.getenv("DBG_NOREADLINE") then
    pcall(function()
      local linenoise = require 'linenoise'
      
      -- Load command history from ~/.lua_history
      local hist_path = os.getenv('HOME') .. '/.lua_history'
      linenoise.historyload(hist_path)
      linenoise.historysetmaxlen(50)
      
      local function autocomplete(env, input, matches)
        for name, _ in pairs(env) do
          if name:match('^' .. input .. '.*') then
            linenoise.addcompletion(matches, name)
          end
        end
      end
      
      -- Auto-completion for locals and globals
      linenoise.setcompletion(function(matches, input)
        -- First, check the locals and upvalues.
        local env = local_bindings(1, true)
        autocomplete(env, input, matches)
        
        -- Then, check the implicit environment.
        env = getmetatable(env).__index
        autocomplete(env, input, matches)
      end)
      
      dbg.read = function(prompt)
        local str = linenoise.linenoise(prompt)
        if str and not str:match "^%s*$" then
          linenoise.historyadd(str)
          linenoise.historysave(hist_path)
        end
        return str
      end
      dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Linenoise support enabled.")
    end)
    
    -- Conditionally enable LuaJIT readline support.
    pcall(function()
      if dbg.read == dbg_read and ffi then
        local readline = ffi.load("readline")
        dbg.read = function(prompt)
          local cstr = readline.readline(prompt)
          if cstr ~= nil then
            local str = ffi.string(cstr)
            if string.match(str, "[^%s]+") then
              readline.add_history(cstr)
            end

            ffi.C.free(cstr)
            return str
          else
            return nil
          end
        end
        dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Readline support enabled.")
      end
    end)
  end

  -- Detect Lua version.
  if jit then -- LuaJIT
    LUA_JIT_SETLOCAL_WORKAROUND = -1
    dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for "..jit.version)
  elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then
    dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for ".._VERSION)
  else
    dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Not tested against ".._VERSION)
    dbg_writeln("Please send me feedback!")
  end

  return dbg

  ''';

function luaopen_debugger(lua: Plua_State): Integer; cdecl;
begin
  if (luaL_loadbufferx(lua, DEBUGGER_SRC, Length(DEBUGGER_SRC), '<debugger.lua>', nil) <> 0) or
     (lua_pcall(lua, 0, LUA_MULTRET, 0) <> 0) then
    lua_error(lua);

  // Or you could load it from disk:
  // if(luaL_dofile(lua, 'debugger.lua')) lua_error(lua);

  Result := 1;
end;

const
  MODULE_NAME: PAnsiChar = 'DEBUGGER_LUA_MODULE';
  MSGH: PAnsiChar = 'DEBUGGER_LUA_MSGH';

procedure dbg_setup(lua: Plua_State; name: PAnsiChar; globalName: PAnsiChar; readFunc: lua_CFunction; writeFunc: lua_CFunction); cdecl;
begin
  // Check that the module name was not already defined.
  lua_getfield(lua, LUA_REGISTRYINDEX, MODULE_NAME);
  Assert(lua_isnil(lua, -1) or (System.AnsiStrings.StrComp(name, luaL_checkstring(lua, -1)) = 0));
  lua_pop(lua, 1);

  // Push the module name into the registry.
  lua_pushstring(lua, name);
  lua_setfield(lua, LUA_REGISTRYINDEX, MODULE_NAME);

  // Preload the module
  luaL_requiref(lua, name, luaopen_debugger, 0);

  // Insert the msgh function into the registry.
  lua_getfield(lua, -1, 'msgh');
  lua_setfield(lua, LUA_REGISTRYINDEX, MSGH);

  if Assigned(readFunc) then
  begin
    lua_pushcfunction(lua, readFunc);
    lua_setfield(lua, -2, 'read');
  end;

  if Assigned(writeFunc) then
  begin
    lua_pushcfunction(lua, writeFunc);
    lua_setfield(lua, -2, 'write');
  end;

  if globalName <> nil then
  begin
    lua_setglobal(lua, globalName);
  end else
  begin
    lua_pop(lua, 1);
  end;
end;

procedure dbg_setup_default(lua: Plua_State); cdecl;
begin
  dbg_setup(lua, 'debugger', 'dbg', nil, nil);
end;

function dbg_pcall(lua: Plua_State; nargs: Integer; nresults: Integer; msgh: Integer): Integer; cdecl;
begin
  // Call regular lua_pcall() if a message handler is provided.
  if msgh <> 0 then
    Exit(lua_pcall(lua, nargs, nresults, msgh));

  // Grab the msgh function out of the registry.
  lua_getfield(lua, LUA_REGISTRYINDEX, PUTF8Char(MSGH));
  if lua_isnil(lua, -1) then
    luaL_error(lua, 'Tried to call dbg_call() before calling dbg_setup().');

  // Move the error handler just below the function.
  msgh := lua_gettop(lua) - (1 + nargs);
  lua_insert(lua, msgh);

  // Call the function.
  Result := lua_pcall(lua, nargs, nresults, msgh);

  // Remove the debug handler.
  lua_remove(lua, msgh);
end;

function dbg_dofile(lua: Plua_State; filename: PAnsiChar): Integer;
begin
  Result := luaL_loadfile(lua, filename);
  if Result = 0 then
    Result := dbg_pcall(lua, 0, LUA_MULTRET, 0);
end;

end.
