#!/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 . --]] --[[ 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) == "" or ssub(err, -7, -1) == "''" 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