summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbuild.sh53
-rw-r--r--src/bin/cratera.lua400
-rw-r--r--src/cratera.cratera.d/lib.cratera9
-rw-r--r--src/cratera/bootstrap.lua4
-rw-r--r--src/cratera/init.lua2
-rw-r--r--test/interp-error.cratera10
6 files changed, 464 insertions, 14 deletions
diff --git a/build.sh b/build.sh
index 6f70eb0..06686dd 100755
--- a/build.sh
+++ b/build.sh
@@ -21,22 +21,52 @@ do_build() {
 	else
 		printf "#!%s %s\n" "$ENV_WRAPPER" "$LUA_INTERPRETER" >out/bin/cratera || exit 1
 	fi
-	cat src/bin/cratera.lua >>out/bin/cratera || exit 1
+	tail -n +2 src/bin/cratera.lua >>out/bin/cratera || exit 1
 	chmod +x out/bin/cratera || exit 1
 	env -i "$LUA_INTERPRETER" src/host/genpath.lua >out/lua/cratera/prebuilt/path.lua || exit 1
 }
 
+test_wrapper() {
+	eval "$@" || {
+		printf 'Test failed:'
+		printf ' %s' "$@"
+		printf '\n'
+		exit 2
+	}
+}
+
 do_test() {
 	# FIXME this does NOT handle LUA_PATH correctly.
 	# FIXME nor LUA_INIT.
 	mkdir -p out/test || exit 2
-	# these tests use the regular lua interpreter
-	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' "$LUA_INTERPRETER" test/testp.lua || exit 2
-	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' "$LUA_INTERPRETER" test/testc.lua || exit 2
-	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' "$LUA_INTERPRETER" test/testbc.lua || exit 2
-	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' CRATERA_PATH='./test/?.cratera;./out/cratera/?.cratera;;' "$LUA_INTERPRETER" test/test_bootstrap.lua || exit 2
-	# these tests use the cratera interpreter
-	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' out/bin/cratera test/tests.cratera || exit 2
+	(
+		export LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;'
+		export CRATERA_PATH='./test/?.cratera;./out/cratera/?.cratera;;'
+		# these tests use the regular lua interpreter
+		"$LUA_INTERPRETER" test/testp.lua || exit 2
+		"$LUA_INTERPRETER" test/testc.lua || exit 2
+		"$LUA_INTERPRETER" test/testbc.lua || exit 2
+		"$LUA_INTERPRETER" test/test_bootstrap.lua || exit 2
+		# these tests use the cratera interpreter
+		test_wrapper 'out/bin/cratera -v | grep Cratera'
+		test_wrapper 'out/bin/cratera -invalid 2>&1 | grep usage'
+		test_wrapper 'out/bin/cratera -void 2>&1 | grep usage'
+		test_wrapper 'out/bin/cratera -e 2>&1 | grep usage'
+		test_wrapper '! out/bin/cratera -e '\''error("hello")'\'''
+		test_wrapper 'out/bin/cratera -e '\''arg[3] = nil'\'' -e '\''print("still prints")'\'
+		test_wrapper 'out/bin/cratera -e '\''print(craterapath)'\'''
+		test_wrapper 'out/bin/cratera test/tests.cratera'
+		test_wrapper 'out/bin/cratera test/interp-error.cratera string 2>&1 | grep '\''5: string'\'
+		test_wrapper 'out/bin/cratera test/interp-error.cratera nil 2>&1 | grep '\''nil value'\'
+		test_wrapper 'out/bin/cratera test/interp-error.cratera table 2>&1 | grep '\''table value'\'
+		test_wrapper 'out/bin/cratera test/interp-error.cratera metaerror 2>&1 | grep '\''overflow\|handling'\'
+		test_wrapper 'printf '\''_PROMPT=setmetatable({}, {__tostring=error})\n'\'' | out/bin/cratera -i 2>&1 ' # | grep '\''overflow\|handling'\'
+	) || exit 2
+}
+
+do_interp() {
+	shift
+	LUA_PATH='./out/lua/?.lua;./out/lua/?/init.lua;;' CRATERA_PATH='./test/?.cratera;./out/cratera/?.cratera;;' out/bin/cratera "$@" || exit 3
 }
 
 case "$1" in
@@ -50,6 +80,13 @@ case "$1" in
 			'The currently selected env wrapper is:' \
 			"$(printf '    ENV_WRAPPER=%s' "$ENV_WRAPPER")"
 		;;
+	interp)
+		do_build
+		do_interp "$@"
+		;;
+	interp-only)
+		do_interp "$@"
+		;;
 	test-only)
 		do_test
 		;;
diff --git a/src/bin/cratera.lua b/src/bin/cratera.lua
index fa94e96..d86f500 100644
--- a/src/bin/cratera.lua
+++ b/src/bin/cratera.lua
@@ -1,5 +1,7 @@
+#!/usr/bin/env lua
+-- previous line gets replaced by build script (for line number correctness)
 --[[
-    Cratera Interpreter
+    Cratera Interpreter/REPL
     Copyright (C) 2024  Soni L.
 
     This program is free software: you can redistribute it and/or modify
@@ -16,6 +18,398 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 --]]
 
-local cratera = require "cratera"
+--[[
+    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))
 
-error("not implemented")
+pcall(exit, status and result)
+if not (status and result) then
+    exit(1)
+end
diff --git a/src/cratera.cratera.d/lib.cratera b/src/cratera.cratera.d/lib.cratera
index 701de31..6e1627e 100644
--- a/src/cratera.cratera.d/lib.cratera
+++ b/src/cratera.cratera.d/lib.cratera
@@ -19,7 +19,7 @@
 local cratera_env = _G
 local cratera_loader_base = cratera_env.load
 
--- only use setfenv if load doesn't support env argument
+-- only use setfenv if lua load doesn't support env argument
 local setfenv = (lua.loadstring or lua.load)("return setfenv", nil, nil, {})()
 
 function cratera_env.load(chunk, chunkname, mode, env)
@@ -38,3 +38,10 @@ function cratera_env.load(chunk, chunkname, mode, env)
     end
 end
 
+-- cratera opts not to support dostring/dofile.
+if lua.dostring then
+    cratera_env.dostring = false
+end
+if lua.dofile then
+    cratera_env.dofile = false
+end
diff --git a/src/cratera/bootstrap.lua b/src/cratera/bootstrap.lua
index 88cef17..c9c7096 100644
--- a/src/cratera/bootstrap.lua
+++ b/src/cratera/bootstrap.lua
@@ -55,11 +55,13 @@ local cratera = require "cratera"
 
 local cratera_env = setmetatable({lua=_G}, {__index=_G})
 cratera_env._G = cratera_env
+cratera_env._VERSION = cratera._VERSION
 -- cratera.lib actually injects itself into *us*!
 package.loaded[...] = cratera_env
 
 local io_open = io.open
 local stdin = io.stdin
+
 -- only use setfenv if load doesn't support env argument
 local setfenv = (loadstring or load)("return setfenv", nil, nil, {})()
 -- we don't know the real BUFSIZ, use a common memory page size (16K, as on M1)
@@ -155,7 +157,7 @@ local function cratera_loadfile(filename, mode, env)
     end
     local func, loader_err = cratera_loader_base(function()
         return reader()
-    end, filename, mode, env)
+    end, chunkname, mode, env)
     local closeok, closeerr = file:close()
     -- at this point, err_or_more_data *should* be nil.
     if err_or_more_data then
diff --git a/src/cratera/init.lua b/src/cratera/init.lua
index a354d56..f7453e4 100644
--- a/src/cratera/init.lua
+++ b/src/cratera/init.lua
@@ -83,4 +83,4 @@ local function cratera_load(reader, ...)
     end, ...)
 end
 
-return {load = cratera_load}
+return {load = cratera_load, _VERSION = "Cratera 2.1 on " .. _VERSION}
diff --git a/test/interp-error.cratera b/test/interp-error.cratera
new file mode 100644
index 0000000..cf177ed
--- /dev/null
+++ b/test/interp-error.cratera
@@ -0,0 +1,10 @@
+-- Test interpreter error handling.
+if arg[1] == "nil" then
+    error(nil)
+elseif arg[1] == "string" then
+    error("string")
+elseif arg[1] == "table" then
+    error({})
+elseif arg[1] == "metaerror" then
+    error(setmetatable({}, {__tostring=error}))
+end