Lua Interpreter In Lua

lua-users home
wiki

Here's is a Lua 5.1 interpreter (lua.c) reimplemented in Lua. The primary goal is to most faithfully reimplement the standard Lua 5.1 interpreter in pure Lua. The goal is a bit different from InteractiveLua, which also implements a Lua interpreter but has differences and extensions. The task here is also partly analogous to LuaCompilerInLua (the Lua compiler front-end, luac.c, partially reimplemented in Lua).

Possible applications

Here's some things this might be used for:

Status

WARNING: This is not completed but was quickly done just an experiment. Fix omissions/bugs and test if you want to use this in production. Particularly pay attention to error handling.

Source

-- lua.lua - Lua 5.1 interpreter (lua.c) reimplemented in Lua.
--
-- WARNING: This is not completed but was quickly done just an experiment.
-- Fix omissions/bugs and test if you want to use this in production.
-- Particularly pay attention to error handling.
--
-- (c) David Manura, 2008-08
-- Licensed under the same terms as Lua itself.
-- Based on lua.c from Lua 5.1.3.
-- Improvements by Shmuel Zeigerman.

-- Variables analogous to those in luaconf.h
local LUA_INIT = "LUA_INIT"
local LUA_PROGNAME = "lua"
local LUA_PROMPT   = "> "
local LUA_PROMPT2  = ">> "
local function LUA_QL(x) return "'" .. x .. "'" end

-- Variables analogous to those in lua.h
local LUA_RELEASE   = "Lua 5.1.3"
local LUA_COPYRIGHT = "Copyright (C) 1994-2008 Lua.org, PUC-Rio"


-- Note: don't allow user scripts to change implementation.
-- Check for globals with "cat lua.lua | luac -p -l - | grep ETGLOBAL"
local _G = _G
local assert = assert
local collectgarbage = collectgarbage
local loadfile = loadfile
local loadstring = loadstring
local pcall = pcall
local rawget = rawget
local select = select
local tostring = tostring
local type = type
local unpack = unpack
local xpcall = xpcall
local io_stderr = io.stderr
local io_stdout = io.stdout
local io_stdin = io.stdin
local string_format = string.format
local string_sub = string.sub
local os_getenv = os.getenv
local os_exit = os.exit


local progname = LUA_PROGNAME

-- Use external functions, if available
local lua_stdin_is_tty = function() return true end
local setsignal = function() end

local function print_usage()
  io_stderr:write(string_format(
  "usage: %s [options] [script [args]].\n" ..
  "Available options are:\n" ..
  "  -e stat  execute string " .. LUA_QL("stat") .. "\n" ..
  "  -l name  require library " .. LUA_QL("name") .. "\n" ..
  "  -i       enter interactive mode after executing " ..
              LUA_QL("script") .. "\n" ..
  "  -v       show version information\n" ..
  "  --       stop handling options\n" ..
  "  -        execute stdin and stop handling options\n"
  ,
  progname))
  io_stderr:flush()
end

local function l_message (pname, msg)
  if pname then io_stderr:write(string_format("%s: ", pname)) end
  io_stderr:write(string_format("%s\n", msg))
  io_stderr:flush()
end

local function report(status, msg)
  if not status and msg ~= nil then
    msg = (type(msg) == 'string' or type(msg) == 'number') and tostring(msg)
          or "(error object is not a string)"
    l_message(progname, msg);
  end
  return status
end

local function tuple(...)
  return {n=select('#', ...), ...}
end

local function traceback (message)
  local tp = type(message)
  if tp ~= "string" and tp ~= "number" then return message end
  local debug = _G.debug
  if type(debug) ~= "table" then return message end
  local tb = debug.traceback
  if type(tb) ~= "function" then return message end
  return tb(message, 2)
end

local function docall(f, ...)
  local tp = {...}  -- no need in tuple (string arguments only)
  local F = function() return f(unpack(tp)) end
  setsignal(true)
  local result = tuple(xpcall(F, traceback))
  setsignal(false)
  -- force a complete garbage collection in case of errors
  if not result[1] then collectgarbage("collect") end
  return unpack(result, 1, result.n)
end

local function dofile(name)
  local f, msg = loadfile(name)
  if f then f, msg = docall(f) end
  return report(f, msg)
end

local function dostring(s, name)
  local f, msg = loadstring(s, name)
  if f then f, msg = docall(f) end
  return report(f, msg)
end

local function dolibrary (name)
  return report(docall(_G.require, name))
end

local function print_version()
  l_message(nil, LUA_RELEASE .. "  " .. LUA_COPYRIGHT)
end

local function getargs (argv, n)
  local arg = {}
  for i=1,#argv do arg[i - n] = argv[i] end
  if _G.arg then
    local i = 0
    while _G.arg[i] do
      arg[i - n] = _G.arg[i]
      i = i - 1
    end
  end
  return arg
end

--FIX? readline support
local history = {}
local function saveline(s)
--  if #s > 0 then
--    history[#history+1] = s
--  end
end


local function get_prompt (firstline)
  -- use rawget to play fine with require 'strict'
  local pmt = rawget(_G, firstline and "_PROMPT" or "_PROMPT2")
  local tp = type(pmt)
  if tp == "string" or tp == "number" then
    return tostring(pmt)
  end
  return firstline and LUA_PROMPT or LUA_PROMPT2
end


local function incomplete (msg)
  if msg then
    local ender = LUA_QL("<eof>")
    if string_sub(msg, -#ender) == ender then
      return true
    end
  end
  return false
end


local function pushline (firstline)
  local prmt = get_prompt(firstline)
  io_stdout:write(prmt)
  io_stdout:flush()
  local b = io_stdin:read'*l'
  if not b then return end -- no input
  if firstline and string_sub(b, 1, 1) == '=' then
    return "return " .. string_sub(b, 2)  -- change '=' to `return'
  else
    return b
  end
end


local function loadline ()
  local b = pushline(true)
  if not b then return -1 end  -- no input
  local f, msg
  while true do  -- repeat until gets a complete line
    f, msg = loadstring(b, "=stdin")
    if not incomplete(msg) then break end  -- cannot try to add lines?
    local b2 = pushline(false)
    if not b2 then -- no more input?
      return -1
    end
    b = b .. "\n" .. b2 -- join them
  end

  saveline(b)

  return f, msg
end


local function dotty ()
  local oldprogname = progname
  progname = nil
  while true do
    local result
    local status, msg = loadline()
    if status == -1 then break end
    if status then
      result = tuple(docall(status))
      status, msg = result[1], result[2]
    end
    report(status, msg)
    if status and result.n > 1 then  -- any result to print?
      status, msg = pcall(_G.print, unpack(result, 2, result.n))
      if not status then
        l_message(progname, string_format(
            "error calling %s (%s)",
            LUA_QL("print"), msg))
      end
    end
  end
  io_stdout:write"\n"
  io_stdout:flush()
  progname = oldprogname
end


local function handle_script(argv, n)
  _G.arg = getargs(argv, n)  -- collect arguments
  local fname = argv[n]
  if fname == "-" and argv[n-1] ~= "--" then
    fname = nil  -- stdin
  end
  local status, msg = loadfile(fname)
  if status then
    status, msg = docall(status, unpack(_G.arg))
  end
  return report(status, msg)
end


local function collectargs (argv, p)
  local i = 1
  while i <= #argv do
    if string_sub(argv[i], 1, 1) ~= '-' then  -- not an option?
      return i
    end
    local prefix = string_sub(argv[i], 1, 2)
    if prefix == '--' then
      if #argv[i] > 2 then return -1 end
      return argv[i+1] and i+1 or 0
    elseif prefix == '-' then
      return i
    elseif prefix == '-i' then
      if #argv[i] > 2 then return -1 end
      p.i = true
      p.v = true
    elseif prefix == '-v' then
      if #argv[i] > 2 then return -1 end
      p.v = true
    elseif prefix == '-e' then
      p.e = true
      if #argv[i] == 2 then
        i = i + 1
        if argv[i] == nil then return -1 end
      end
    elseif prefix == '-l' then
      if #argv[i] == 2 then
        i = i + 1
        if argv[i] == nil then return -1 end
      end
    else
      return -1  -- invalid option
    end
    i = i + 1
  end
  return 0
end


local function runargs(argv, n)
  local i = 1
  while i <= n do if argv[i] then
    assert(string_sub(argv[i], 1, 1) == '-')
    local c = string_sub(argv[i], 2, 2) -- option
    if c == 'e' then
      local chunk = string_sub(argv[i], 3)
      if chunk == '' then i = i + 1; chunk = argv[i] end
      assert(chunk)
      if not dostring(chunk, "=(command line)") then return false end
    elseif c == 'l' then
      local filename = string_sub(argv[i], 3)
      if filename == '' then i = i + 1; filename = argv[i] end
      assert(filename)
      if not dolibrary(filename) then return false end
    end
    i = i + 1
  end end
  return true
end


local function handle_luainit()
  local init = os_getenv(LUA_INIT)
  if init == nil then
    return  -- status OK
  elseif string_sub(init, 1, 1) == '@' then
    dofile(string_sub(init, 2))
  else
    dostring(init, "=" .. LUA_INIT)
  end
end


local import = _G.import
if import then
  lua_stdin_is_tty = import.lua_stdin_is_tty or lua_stdin_is_tty
  setsignal        = import.setsignal or setsignal
  LUA_RELEASE      = import.LUA_RELEASE or LUA_RELEASE
  LUA_COPYRIGHT    = import.LUA_COPYRIGHT or LUA_COPYRIGHT
  _G.import = nil
end

if _G.arg and _G.arg[0] and #_G.arg[0] > 0 then progname = _G.arg[0] end
local argv = {...}
handle_luainit()
local has = {i=false, v=false, e=false}
local script = collectargs(argv, has)
if script < 0 then -- invalid args?
  print_usage()
  os_exit(1)
end
if has.v then print_version() end
local status = runargs(argv, (script > 0) and script-1 or #argv)
if not status then os_exit(1) end
if script ~= 0 then
  status = handle_script(argv, script)
  if not status then os_exit(1) end
else
  _G.arg = nil
end
if has.i then
  dotty()
elseif script == 0 and not has.e and not has.v then
  if lua_stdin_is_tty() then
    print_version()
    dotty()
  else dofile(nil)  -- executes stdin as a file
  end
end

See Also


RecentChanges · preferences
edit · history
Last edited August 14, 2023 11:19 pm GMT (diff)