diff options
-rwxr-xr-x | build.sh | 6 | ||||
-rw-r--r-- | src/cratera.cratera.d/lib.cratera | 40 | ||||
-rw-r--r-- | src/cratera/bootstrap.lua | 258 | ||||
-rw-r--r-- | src/cratera/compiler.lua | 8 | ||||
-rw-r--r-- | src/cratera/init.lua | 9 | ||||
-rw-r--r-- | src/host/genpath.lua | 25 | ||||
-rw-r--r-- | test/crateratests.cratera.d/bootstrap.cratera | 7 | ||||
-rw-r--r-- | test/luafile.lua | 8 | ||||
-rw-r--r-- | test/test_bootstrap.lua | 9 |
9 files changed, 363 insertions, 7 deletions
diff --git a/build.sh b/build.sh index 9b78d8c..6f70eb0 100755 --- a/build.sh +++ b/build.sh @@ -10,10 +10,12 @@ case "$LUA_INTERPRETER" in esac do_build() { - mkdir -p out/lua/cratera/ || exit 1 + mkdir -p out/lua/cratera/prebuilt/ || exit 1 + mkdir -p out/cratera/cratera.cratera.d || exit 1 mkdir -p out/bin || exit 1 # worst tool for this job cp -R src/cratera out/lua/ || exit 1 + cp -R src/cratera.cratera.d out/cratera/ || exit 1 if [ "$ENV_WRAPPER" = '' ]; then printf "#!%s\n" "$LUA_INTERPRETER" >out/bin/cratera || exit 1 else @@ -21,6 +23,7 @@ do_build() { fi cat 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 } do_test() { @@ -31,6 +34,7 @@ do_test() { 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 } diff --git a/src/cratera.cratera.d/lib.cratera b/src/cratera.cratera.d/lib.cratera new file mode 100644 index 0000000..701de31 --- /dev/null +++ b/src/cratera.cratera.d/lib.cratera @@ -0,0 +1,40 @@ +--[[ + Cratera Library + 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/>. +--]] + +local cratera_env = _G +local cratera_loader_base = cratera_env.load + +-- only use setfenv if 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) + if env == nil then + env = cratera_env + end + local func, err = cratera_loader_base(chunk, chunkname, mode, env) + if setfenv and func and env then + setfenv(func, env) + end + -- arity adjustments + if func then + return func + else + return func, err + end +end + diff --git a/src/cratera/bootstrap.lua b/src/cratera/bootstrap.lua new file mode 100644 index 0000000..88cef17 --- /dev/null +++ b/src/cratera/bootstrap.lua @@ -0,0 +1,258 @@ +--[[ + Cratera Bootstrap + 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. +--]] + +-- The Cratera Bootstrap library sets up Cratera "proper", not just the +-- compiler. However, it also expects a vanilla(-compatible) Lua environment. +-- In particular, it uses the io library to access the filesystem. +-- It also expects _G to be the default environment. + +-- NOTE: file:read() has the a-bit-annoying property of resetting EOF and other +-- errors every time it's called, so we must pass (n, 0) if we want a reliable +-- EOF indicator (which we do). + +local cratera = require "cratera" + +local cratera_env = setmetatable({lua=_G}, {__index=_G}) +cratera_env._G = cratera_env +-- 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) +local BUFSIZ = 16384 + +local cratera_loader_base = cratera.load +-- it is convenient to set cratera_env.load +-- it will be replaced with the proper function in cratera.lib +cratera_env.load = cratera_loader_base + +-- duplicated with init.lua +local LUA_SIGNATURE +if string.dump then + LUA_SIGNATURE = string.dump(function() end):sub(1,1) +else + LUA_SIGNATURE = string.char(27) +end + +-- based on lauxlib.c, PUC Rio +local function skip_interpreter_line(file) + -- lua skips UTF-8 BOM, we do not. + -- join us, reject the BOM. + local initial_buf = "" + -- see NOTE at the start of this file + local cdata, err_or_more_data = file:read(1, 0) + if cdata == "#" and err_or_more_data then + -- skip interpreter line + repeat + cdata, err_or_more_data = file:read(1, 0) + until cdata == "\n" or not cdata or not err_or_more_data + -- align line numbers so error messages are correct! + initial_buf = initial_buf .. "\n" + if cdata and err_or_more_data then + cdata, err_or_more_data = file:read(1, 0) + end + end + return initial_buf, cdata, err_or_more_data +end + +local function cratera_loadfile(filename, mode, env) + if env == nil then + -- load default env - if it doesn't exist, load lua env + env = cratera_env + end + local file, err + if filename ~= nil then + file, err = io_open(filename, "r") + if not file then + return nil, ("cannot open %s: %s"):format(filename, err) + end + else + file = stdin + end + local initial_buf, cdata, err_or_more_data = skip_interpreter_line(file) + if cdata == LUA_SIGNATURE and err_or_more_data and filename then + -- binary file + file:close() + file, err = io_open(filename, "rb") + if not file then + return nil, ("cannot reopen %s: %s"):format(filename, err) + end + initial_buf, cdata, err_or_more_data = skip_interpreter_line(file) + end + if cdata then + initial_buf = initial_buf .. cdata + end + -- must declare and set separately + -- this is a bit involved. importantly: the chunk (passed to load()) always + -- tail calls into the "reader", however we redefine "reader" based on + -- various condtions (on first call, on eof, on error) + local reader + reader = function() + reader = function() + local buf + buf, err_or_more_data = file:read(BUFSIZ, 0) + if not buf or not err_or_more_data then + reader = function() end + end + if buf == "" then return reader() end + return buf + end + if not err_or_more_data then + reader = function() end + end + if initial_buf == "" then return reader() end + return initial_buf + end + local chunkname + if not filename then + chunkname = "=stdin" + else + chunkname = "@" .. filename + end + local func, loader_err = cratera_loader_base(function() + return reader() + end, filename, mode, env) + local closeok, closeerr = file:close() + -- at this point, err_or_more_data *should* be nil. + if err_or_more_data then + -- unless it's an error + return nil, ("cannot read %s: %s"):format(filename, err_or_more_data) + end + -- lua doesn't quite check this properly but it's possible for an error to + -- be delayed until close + if not closeok and closeerr then + return nil, ("cannot read %s: %s"):format(filename, closeerr) + end + if setfenv and func and env then + setfenv(func, env) + end + -- arity adjustments + if func then + return func + else + return func, loader_err + end +end + +cratera_env.loadfile = cratera_loadfile + +local searchers = package.searchers or package.loaders +local dirsep, pathsep, pathmark = package.config:match("^([^\n]+)\n([^\n]+)\n([^\n]+)") +local CRATERA_SEP = ".cratera.d" .. dirsep +local tconcat = table.concat +do + -- discover the cratera path + local default_path = require "cratera.prebuilt.path" + local cratera_path_env = os.getenv("CRATERA_PATH") + + if cratera_path_env then + local default_mark = cratera_path_env:find(pathsep..pathsep, 1, true) + if not default_mark then + cratera_env.craterapath = cratera_path_env + else + local left = cratera_path_env:sub(1, default_mark - 1) + local right = cratera_path_env:sub(default_mark + 2) + if #left > 0 and #right > 0 then + cratera_env.craterapath = left .. ";" .. default_path .. ";" .. right + elseif #left > 0 then + cratera_env.craterapath = left .. ";" .. default_path + elseif #right > 0 then + cratera_env.craterapath = default_path .. ";" .. right + else + cratera_env.craterapath = default_path + end + end + else + cratera_env.craterapath = default_path + end +end + +local error = error +local type = type +local assert = assert + +local psearchpath = package.searchpath or (function() + assert(#pathsep == 1, "unsupported lua configuration") + assert(#pathmark == 1, "unsupported lua configuration") + local pathfinder = "[^"..pathsep.."]*" + return function(name, path, sep, rep) + assert(sep == ".") + -- table trick is safe to use with "untrusted" input (e.g. dirsep) + local filename = name:gsub("%.", {["."]=rep}) + -- this is a bit inefficient but it's the best we can do (safely)... + local filepaths = path:gsub(".", {[pathmark]=filename}) + -- this is safe given the previous assertions + for filepath in filepaths:gmatch(pathfinder) do + local file = io_open(filepath) + if file then + file:close() + return filepath + end + end + return nil, "no file '" .. filepaths:gsub(".", {[pathsep]="\n\tno file'"}) .. "'" + end +end)() + +-- put cratera searcher first, before lua searcher +table.insert(searchers, 2, function(modname) + if type(cratera_env.craterapath) ~= "string" then + error("craterapath must be a string") + end + local filename, err = psearchpath(modname, cratera_env.craterapath, ".", CRATERA_SEP) + if not filename then + return err + end + local mod, err = cratera_loadfile(filename) + if mod then + return mod, filename + else + error(("error loading module '%s' from file '%s':\n\t%s"):format(modname, filename, err)) + end +end) + +-- load the cratera lib. +require "cratera.lib" diff --git a/src/cratera/compiler.lua b/src/cratera/compiler.lua index 2f4a998..cb0681f 100644 --- a/src/cratera/compiler.lua +++ b/src/cratera/compiler.lua @@ -1,6 +1,6 @@ --[[ This file is part of Cratera Compiler - Copyright (C) 2019 Soni L. + Copyright (C) 2019, 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 @@ -42,7 +42,9 @@ local END_OF_STMT = {} local END_OF_CRATERA = {} -- implementation of myobj:[mytrait].myfunction(...) -local CRATERA_FUNCTION = "function(myobj, mytrait, myfunction, ...) return myobj[mytrait][myfunction](myobj, ...) end" +-- TODO benchmark under luajit? +local CRATERA_FUNCTION = "_CRATERA_INVOKE_TRAIT" +local CRATERA_INIT = "local function "..CRATERA_FUNCTION.."(myobj, mytrait, myfunction, ...) return myobj[mytrait][myfunction](myobj, ...) end " local EXTRA_DATA = {[TK.NAME] = true, [TK.INT] = true, [TK.FLT] = true, [TK.STRING] = true, [END_OF_STMT] = true} @@ -134,7 +136,7 @@ defs[parser.EOZ] = function(state, token) return FINISH end defs[parser.FALLBACK] = function(state, token) - local results = state.results or (function() state.results = {} return state.results end)() + local results = state.results or (function() state.results = {CRATERA_INIT, n=1} return state.results end)() do -- handle newlines. this allows error messages to correctly map between lua and cratera local oldline = state.oldline or 1 diff --git a/src/cratera/init.lua b/src/cratera/init.lua index bac3f53..a354d56 100644 --- a/src/cratera/init.lua +++ b/src/cratera/init.lua @@ -16,13 +16,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. --]] --- This code is highly experimental and not very good - local parser = require "cratera.parser" local luatokens = require "cratera.luatokens" local compiler = require "cratera.compiler" -local LUA_SIGNATURE = string.dump(function() end):sub(1,1) +local LUA_SIGNATURE +if string.dump then + LUA_SIGNATURE = string.dump(function() end):sub(1,1) +else + LUA_SIGNATURE = string.char(27) +end local function cratera_load(reader, ...) if type(reader) == "string" and reader:sub(1,1) == LUA_SIGNATURE then diff --git a/src/host/genpath.lua b/src/host/genpath.lua new file mode 100644 index 0000000..e12e9a8 --- /dev/null +++ b/src/host/genpath.lua @@ -0,0 +1,25 @@ +-- generate default cratera path +-- package.path, but every .lua is replaced with .cratera +-- or use an explicit CRATERA_PATH + +if os.getenv("DEFAULT_CRATERA_PATH") then + print(string.format("return %q", os.getenv("DEFAULT_CRATERA_PATH"))) + return +end + +local pathsep = package.config:match("^[^\n]+\n([^\n]+)") +local pathfinder = "[^"..pathsep.."]*" +-- not gonna worry about ".lua.lua" tbh... +local possible_cratera_path = package.path:gsub("%.lua(.)", {[pathsep] = ".cratera" .. pathsep}):gsub("%.lua$",".cratera") +local path_parts = {} +local i = 1 +-- path entries to skip based on filename +local INIT_FILE = "init.cratera" +for path_part in possible_cratera_path:gmatch(pathfinder) do + if not path_part:find(INIT_FILE, -#INIT_FILE, true) then + path_parts[i] = path_part + i = i + 1 + end +end + +print(string.format("return %q", table.concat(path_parts, pathsep))) diff --git a/test/crateratests.cratera.d/bootstrap.cratera b/test/crateratests.cratera.d/bootstrap.cratera new file mode 100644 index 0000000..618b0fb --- /dev/null +++ b/test/crateratests.cratera.d/bootstrap.cratera @@ -0,0 +1,7 @@ +-- test that cratera can require lua modules + +require "test.luafile" + +-- also check that we do have craterapath + +assert(craterapath) diff --git a/test/luafile.lua b/test/luafile.lua new file mode 100644 index 0000000..3ec81cd --- /dev/null +++ b/test/luafile.lua @@ -0,0 +1,8 @@ +-- yes, this is a very small test file and it doesn't get invoked by +-- build.sh directly. +-- +-- it's used to check if lua modules can still be used from cratera. + +print "all good" + +assert(not craterapath) diff --git a/test/test_bootstrap.lua b/test/test_bootstrap.lua new file mode 100644 index 0000000..7edad26 --- /dev/null +++ b/test/test_bootstrap.lua @@ -0,0 +1,9 @@ +-- test cratera.bootstrap functions + +local bootstrap = require "cratera.bootstrap" + +-- loadfile is defined in bootstrap +assert(bootstrap.loadfile("test/tests.cratera")) + +-- bootstrap sets up a loader +require "crateratests.bootstrap" |