#!/usr/bin/env lua
-- previous line gets replaced by build script (for line number correctness)
--[[
Cratera Interpreter/REPL
Copyright (C) 2024 Soni L.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
--]]
--[[
Parts of this file are based on PUC-Rio Lua, available under the following
license:
* Copyright (C) 1994-2018 Lua.org, PUC-Rio.
*
* 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.
--]]
local cratera = require "cratera.bootstrap"
local lua = _G
local arg = _G.arg
local progname = arg[0] or "cratera"
local ssub = string.sub
local exit = os.exit
local pcall = pcall
local xpcall = xpcall
local assert = assert
--local assert = function(...) return ... end
local traceback = debug.traceback
local type = type
local tostring = tostring
local dtraceback = debug.traceback
local dgetmetatable = debug.getmetatable
local rawget = rawget
local tunpack = table.unpack or unpack
local tconcat = table.concat
local warn = _G.warn
local cratera_load = cratera.load
local cratera_loadfile = cratera.loadfile
local cratera_VERSION = cratera._VERSION
local getenv = os.getenv
local error = error
local sfind = string.find
local collectgarbage = collectgarbage
local select = select
local stdin, stdout, stderr = io.stdin, io.stdout, io.stderr
local _ENV = nil
-- parse arguments
local i = 1
local erropt = 1
local interactive, version, exec, warnings, opterror, script
while arg[i] do
erropt = i
local argv = arg[i]
if argv == "--" or argv == "-" then
break
elseif argv == "-i" then
interactive = true
version = true
elseif argv == "-v" then
version = true
elseif warn and argv == "-W" then
warnings = true
elseif ssub(argv, 1, 1) == "-" then
local argkey = ssub(argv, 2, 2)
if argkey == "e" then
exec = true
end
if argkey == "e" or argkey == "l" then
if #argv == 2 then i = i + 1 end
if not arg[i] or ssub(arg[i], 1, 1) == "-" then
opterror = true
break
end
else
opterror = true
break
end
else
script = i
break
end
i = i + 1
end
-- print usage
if opterror then
stderr:write(progname .. ": ")
local optkey = ssub(arg[erropt], 2, 2)
if optkey == "e" or optkey == "l" then
stderr:write("'" .. arg[erropt] .. "' needs argument\n")
else
stderr:write("unrecognized option '" .. arg[erropt] .. "'\n")
end
stderr:write(tconcat {
("usage: " .. progname .. "[options] [script [args]]\n"),
("Available options are:\n"),
(" -e stat execute string 'stat'\n"),
(" -i enter interactive mode after executing 'script'\n"),
(" -l mod require library 'mod' into global 'mod'\n"),
(" -l g=mod require library 'mod' into global 'g'\n"),
(" -v show version information\n"),
(" -W turn warnings on\n"):sub(1, warn and -1 or 0),
(" -- stop handling options\n"),
(" - stop handling options and execute stdin\n"),
})
stderr:flush()
pcall(exit, false)
exit(1)
end
-- do we have a script? if so, shift args now
local cratera_first_arg = 1
local cratera_last_arg = #arg
if script then
local minarg = 0
while arg[minarg] do
minarg = minarg - 1
end
minarg = minarg + 1
local oldarg = minarg
local newarg = minarg - script
while arg[oldarg] do
arg[newarg] = arg[oldarg]
oldarg, newarg = oldarg + 1, newarg + 1
end
while arg[newarg] do
arg[newarg] = nil
newarg = newarg + 1
end
-- the goal is to have the script name at arg[0]
-- while the original arg[0] was the cratera repl/interpreter
-- thus, arg[-script] should end up as the cratera repl/interpreter
assert(arg[-script] == progname)
cratera_first_arg = -script + 1
cratera_last_arg = -1
script = arg[0]
end
local function report(status, ...)
if not status then
if progname then
stderr:write(progname, ": ")
end
stderr:write(..., "\n")
stderr:flush()
end
return status, ...
end
local function do_call(wrapper_func)
return report(xpcall(function()
return wrapper_func()
end, function(err)
if type(err) ~= "string" then
if dgetmetatable then
local meta = dgetmetatable(err)
if type(meta) == "table" and rawget(meta, "__tostring") then
err = rawget(meta, "__tostring")(err)
end
end
if type(err) ~= "string" then
err = "(error object is a " .. type(err) .. " value)"
end
end
return dtraceback(err)
end))
end
local function do_chunk(func, err)
if report(func, err) then
return do_call(func)
end
return func, err
end
local function do_string(chunk, name)
return do_chunk(cratera_load(chunk, name))
end
local function do_file(filename)
return do_chunk(cratera_loadfile(filename))
end
local function do_init()
if getenv("CRATERA_INIT") then
local init_script = getenv("CRATERA_INIT")
if ssub(init_script, 1, 1) == "@" then
return do_file(ssub(init_script, 2))
else
return do_string(init_script, "=CRATERA_INIT")
end
else
return true
end
end
local function do_library(mod)
local globalname = sfind(mod, "=", 1, true)
if globalname then
globalname, mod = ssub(mod, 1, globalname - 1), ssub(mod, globalname + 1)
else
globalname = mod
end
-- N.B. use cratera.require and NOT lua.require.
-- (by default, cratera.require gets proxied to lua.require, but a
-- previous -e/-l may override it)
local require = cratera.require
return do_call(function()
cratera[globalname] = require(mod)
end)
end
local function do_args()
-- -e/-l may modify global arg
local origarg = arg
local arg = {}
for i=cratera_first_arg,cratera_last_arg do
arg[i] = origarg[i]
end
local i = cratera_first_arg
while i < cratera_last_arg do
assert(ssub(arg[i], 1, 1) == "-")
local key = ssub(arg[i], 2, 2)
if key == "e" or key == "l" then
local extra = ssub(arg[i], 3)
if extra == "" then
i = i + 1
extra = arg[i]
assert(extra ~= nil)
end
local ok
if key == "e" then
ok = do_string(extra, "=(command line)")
elseif key == "l" then
ok = do_library(extra)
end
if not ok then return false end
elseif key == "W" then
warn("@on")
end
i = i + 1
end
return true
end
local function do_version()
stdout:write(cratera_VERSION, "\n")
stdout:flush()
end
local function do_script()
local func, err = cratera_loadfile(script)
local arg = cratera.arg
if type(arg) ~= "table" then
error("'arg' is not a table")
end
if func then
return do_call(function()
return func(tunpack(arg))
end)
end
return report(func, err)
end
local function do_repl()
local function get_prompt(first_line)
local ok, prompt = do_call(function()
local prompt = cratera[first_line and "_PROMPT" or "_PROMPT2"]
if prompt == nil then
return nil
end
return tostring(prompt)
end)
if not ok or prompt == nil then
return first_line and "> " or ">> "
else
return prompt
end
end
local function readline(first_line)
local prompt = get_prompt(first_line)
stdout:write(prompt)
stdout:flush()
local ok, line = stdin:read(0, "*l")
if not ok and not line then
return nil
elseif ok then
return line
else
error("NYI: read error")
end
end
local function incomplete(ok, err)
if not ok then
return ssub(err, -5, -1) == "<eof>" or ssub(err, -7, -1) == "'<eof>'"
end
return false
end
local function loadline()
local line = readline(true)
if line == nil then return nil end
local retline = "return " .. line .. ";"
local ok, err = cratera_load(retline, "=stdin")
if ok then
return ok
end
-- handle multiline, normal chunks, etc
while true do
local ok, err = cratera_load(line, "=stdin")
if not incomplete(ok, err) then
return ok, err
end
local continuation = readline(false)
if not continuation then
return ok, err
end
line = line .. "\n" .. continuation
end
end
local function l_print(ok, ...)
if not ok then return end
if select("#", ...) > 0 then
local ok, err = pcall(cratera.print, ...)
if not ok then
if type(err) ~= "string" then
err = "(error object is a " .. type(err) .. " value)"
end
stderr:write("error calling 'print' (" .. err .. ")\n")
end
end
end
local oldprogname = progname
progname = nil
while true do
local line, err = loadline()
if line == nil and err == nil then
break
end
if line then
l_print(do_call(line))
else
report(line, err)
end
end
stdout:write('\n')
stdout:flush()
end
local function do_main()
if version then
do_version()
end
if not do_init() then
return false
end
if not do_args() then
return false
end
if script then
if not do_script() then
return false
end
end
if interactive then
do_repl()
elseif (not script) and not (exec or version) then
-- no pure lua tty detection :/
do_version()
do_repl()
end
return true
end
local status, result = report(pcall(do_main))
pcall(exit, status and result)
if not (status and result) then
exit(1)
end