diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2024-06-04 23:48:59 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2024-06-04 23:48:59 -0300 |
commit | dba1285ca98d7a325f05b77b805089b3edd61867 (patch) | |
tree | f5ddfbd1c86a7db464dc1c459c38c5e256e49089 | |
parent | 5eecbd6ef771a54b455d96f4033212062d7c3f8f (diff) |
Implement cratera REPL
-rwxr-xr-x | build.sh | 53 | ||||
-rw-r--r-- | src/bin/cratera.lua | 400 | ||||
-rw-r--r-- | src/cratera.cratera.d/lib.cratera | 9 | ||||
-rw-r--r-- | src/cratera/bootstrap.lua | 4 | ||||
-rw-r--r-- | src/cratera/init.lua | 2 | ||||
-rw-r--r-- | test/interp-error.cratera | 10 |
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 |