summary refs log blame commit diff stats
path: root/src/bin/cratera.lua
blob: d86f5006fe98f4cc106e2eb0d8ed5d563789698e (plain) (tree)
1
2
3
4

                                                                            
    
                            















                                                                               





































































































































































































































































































































































































                                                                                 
 



                               
#!/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