commit 596fc9e9ced7d5120d9fe67469f3cc5c50b3e6b5 Author: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Thu Feb 6 02:30:52 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb52eeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/* +!/addon/ +!/build/ +!/core/ +!/mod/ +!/.gitignore +!/Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ba8452 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: clean install-mods install + +install: clean install-mods + luajit build/init.lua --linux + +install-mods: clean + rm -rf ~/.local/lib/.tal_mod + mkdir -p ~/.local/lib/.tal_mod + cp -r mod/* ~/.local/lib/.tal_mod/ + +clean: + rm -rf ~/.local/bin/tal + rm -rf ~/.local/lib/.tal_mod diff --git a/addon/TAL/config.json b/addon/TAL/config.json new file mode 100644 index 0000000..cf7fdbc --- /dev/null +++ b/addon/TAL/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/LLS-Addons/main/schemas/addon_config.schema.json", + "words": ["array%s*%{"], + "settings": { + "Lua.runtime.version" : "LuaJIT", + "Lua.diagnostics.globals" : [ + "global1", + "global2" + ], + "Lua.runtime.special" : { + "import" : "require" + } + } +} diff --git a/addon/TAL/library/array.lua b/addon/TAL/library/array.lua new file mode 100644 index 0000000..b1b184c --- /dev/null +++ b/addon/TAL/library/array.lua @@ -0,0 +1,123 @@ +--- @meta + +--- @alias array { [integer]: T } | arraylib + +--- @class arraylib +arrays = {}; + +--- Converts the object to an array by putting "arrays" as its metatable +--- @generic T +--- @param obj { [integer]: T } +--- @return array +--- @overload fun(val: string): array +function array(obj) end + +--- Converts the string to an array of characters +--- @param str string +--- @return array +function array(str) end + +--- Returns all the given arrays, concatenated to one +--- @generic T +--- @param ... T[] +--- @return array +function arrays.concat(...) end + +--- @generic T +--- @param self T[] +--- @param ... T[] +--- @return self +function arrays.append(self, ...) end + +--- Adds all the given elements to the end of this array +--- @generic T +--- @param self T[] +--- @param ... T[] +--- @return self +function arrays.push(self, ...) end + +--- Removes the last element of the array and returns it +--- @generic T +--- @param self array +--- @return T val? The removed element, or nil if none +function arrays:pop() end + +--- Returns the last element of the array +--- @generic T +--- @param self array +--- @return T val? The last element of this array, or nil of none +function arrays:peek() end + +--- Removes the first element of this array +--- @generic T +--- @param self array +--- @return T val? The last element of this array, or nil of none +function arrays.shift(self) end + +--- Adds all the given elements to the end of this array +--- @generic T +--- @param self T[] +--- @param ... T[] +--- @return self +function arrays.unshift(self, ...) end + +--- Returns the result of mapping the values in table t through the function f +--- @generic In, Out +--- @param self In[] +--- @param f fun(val: In, i: integer, self: self): Out +--- @param mutate? boolean If true, will operate directly on the given array +--- @return Out[] arr +function arrays.map(self, f, mutate) end + +--- Like arrays:map, but will expect the function to return arrays, and will :append them to the result array instead +--- @generic In, Out +--- @param self In[] +--- @param f fun(val: In, i: integer, self: self): Out[] +--- @return Out[] arr +function arrays.flat_map(self, f) end + +--- Sorts the array in ascending order +--- @generic T +--- @param self T[] +--- @param f? fun(a: T, b: T): boolean A "less than" function, aka, a < b +--- @param copy? boolean If true will operate on a copy of the array +--- @return T[] +function arrays.sort(self, f, copy) end + +--- Finds the index of the given element, or nil if it doesn't exist +--- @generic T +--- @param self T[] +--- @param f? fun(val: T, i: integer, self: self): T The predicate +--- @return integer? +function arrays.find_i(self, f) end + +--- Sets each value from b to e to val in the given array +--- @generic T +--- @param self T[] +--- @param b? integer +--- @param e? integer +--- @return self +function arrays.fill(self, val, b, e) end + +--- Every element from start to stop is removed from this array and are replaced with the given elements +--- @generic T +--- @param self T[] +--- @param b? integer +--- @param e? integer +--- @param ... T +--- @return self +function arrays.splice(self, b, e, ...) end + +--- Returns the subarray from b to e +--- @generic T +--- @param self T[] +--- @param b? integer +--- @param e? integer +--- @return T[] +function arrays.slice(self, b, e) end + +--- Equivalent of table.concat(self, sep, b, e) +--- @param sep string? Separator (defaults to empty) +--- @param b number? First element to take (defaults to beginning) +--- @param e number? Last element to take (defaults to end) +function arrays:join(sep, b, e) end diff --git a/addon/TAL/library/coro.lua b/addon/TAL/library/coro.lua new file mode 100644 index 0000000..1b63cf7 --- /dev/null +++ b/addon/TAL/library/coro.lua @@ -0,0 +1,94 @@ +--- @meta + +--- @class coro +coro = {}; + +--- Creates a symmetric coroutine from the function +--- @param func function +--- @return thread +function coro.create(func) end + +--- @return thread th The currently running thread +--- @return boolean is_main Whether or not this is the main thread +function coro.running() end + +--- Gets the status of the thread +--- @param th thread +--- @return +--- | "running" -- The thread is the currently running one +--- | "suspended" -- The thread is not running, but may be resumed +--- | "normal" -- Is active but not running. +--- | "dead" -- The coroutine is not running and can't be resumed +function coro.status(th) end + +--- Transfers the execution to the given thread +--- @param th thread The thread to transfer execution to +--- @param ... any The arguments to pass to the thread +--- @return boolean ok Whether or not the thread yielded normally +--- @return ... Result if ok is true, the error if ok is false +function coro.ptransfer(th, ...) end + +--- Transfers the execution to the given thread +--- @param th thread The thread to transfer execution to +--- @param ... any The arguments to pass to the thread +--- @return ... The yielded values form the function +function coro.transfer(th, ...) end + +--- Transfers the execution to the given thread +--- @param f fun(yield: fun(...): nil, ...): ... Wrapped function +--- @return fun(...): ... +function coro.wrap(f) end + +--- @param f fun(yield: (fun(...): ...), ...): ... +--- @return fun(...): fun(...): ... +function coro.gen(f) end + +--- Prematurely kills the coroutine +--- @param th thread +--- @return boolean ok +--- @return string? err +function coro.close(th) end + +--- @class coroutine +coroutine = {}; + +--- Creates a asymmetric coroutine from the function +--- @param func function +--- @return thread +function coroutine.create(func) end + +--- @return thread th The currently running thread +--- @return boolean is_main Whether or not this is the main thread +function coroutine.running() end + +--- Gets the status of the thread +--- @param th thread +--- @return +--- | "running" -- The thread is the currently running one +--- | "suspended" -- The thread is not running, but may be resumed +--- | "normal" -- Is active but not running. +--- | "dead" -- The coroutine is not running and can't be resumed +function coroutine.status(th) end + +--- Transfers to the coroutine asymmetrically; If it uses an asymmetric yield it will transfer to this coroutine +--- @param th thread The thread to transfer execution to +--- @param ... any The arguments to pass to the thread +--- @return boolean ok Whether or not the thread yielded normally +--- @return ... Result if ok is true, the error if ok is false +function coroutine.resume(th, ...) end + +--- Yields to the calling coroutine +--- @param ... any The arguments to pass to the thread +--- @return ... The yielded values form the function +function coroutine.yield(...) end + +--- Transfers the execution to the given thread +--- @param f fun(...): ... Wrapped function +--- @return fun(...): ... +function coroutine.wrap(f) end + +--- Prematurely kills the coroutine +--- @param th thread +--- @return boolean ok +--- @return string? err +function coroutine.close(th) end diff --git a/addon/TAL/library/env.lua b/addon/TAL/library/env.lua new file mode 100644 index 0000000..192f350 --- /dev/null +++ b/addon/TAL/library/env.lua @@ -0,0 +1,8 @@ +--- @meta + +--- @class env +--- @field jit boolean +--- @field os string +--- @field runtime string +--- @field arch string +env = {}; diff --git a/addon/TAL/library/function.lua b/addon/TAL/library/function.lua new file mode 100644 index 0000000..a56062d --- /dev/null +++ b/addon/TAL/library/function.lua @@ -0,0 +1,26 @@ +--- @meta + +--- @class functionlib +functions = {}; + +--- Constructs a function, such that it calls the first function with the passed arguments, +--- the second function with the return of the first, and so on. The return value of the last function is returned +--- +--- In short, does the following, if the passed functions are a, b and c: return c(b(a(...))) +--- +--- Sometimes less cumbersome to write (a | b | c | d)(args...) than d(c(b(a(args...)))) +--- @param self function +function functions:pipe(...) end +function functions:apply(args) end +--- Constructs a function, such that it calls the first function with the passed arguments, +--- the second function with the return of the first, and so on. The return value of the last function is returned +--- +--- In short, does the following, if the passed functions are a, b and c: return c(b(a(...))) +--- +--- Sometimes less cumbersome to write (a | b | c | d)(args...) than d(c(b(a(args...)))) +--- @param self function +function functions:pcall(...) end +--- Calls pipe with a and b; Alternative syntax for older Lua installations +function functions.__sub(a, b) end +--- Calls pipe with a and b +function functions.__bor(a, b) end diff --git a/addon/TAL/library/module.lua b/addon/TAL/library/module.lua new file mode 100644 index 0000000..3d3c045 --- /dev/null +++ b/addon/TAL/library/module.lua @@ -0,0 +1,6 @@ +--- @meta + +module = { exports = {}, require = require, import = function (id) end, export = function (obj) end }; +exports = module.exports; +import = require; +export = module.export; diff --git a/addon/TAL/library/polyfill.lua b/addon/TAL/library/polyfill.lua new file mode 100644 index 0000000..9dd2de6 --- /dev/null +++ b/addon/TAL/library/polyfill.lua @@ -0,0 +1,4 @@ +--- @meta + +unpack = table.unpack; +table.unpack = unpack; diff --git a/addon/TAL/library/printing.lua b/addon/TAL/library/printing.lua new file mode 100644 index 0000000..26a4239 --- /dev/null +++ b/addon/TAL/library/printing.lua @@ -0,0 +1,10 @@ +--- @meta + +--- Converts the value to a readable string +function to_readable(val, indent_str) end + +--- Prints the given values to stderr +function print(...) end + +--- Pretty-prints the given values to stderr +function pprint(...) end diff --git a/addon/TAL/library/string.lua b/addon/TAL/library/string.lua new file mode 100644 index 0000000..436d916 --- /dev/null +++ b/addon/TAL/library/string.lua @@ -0,0 +1,24 @@ +--- @meta + + +--- Splits the given string with the given delimiter +--- @param self string +--- @param sep string The delimiter to split by. Defaults to an empty string +--- @return array +function string.split(self, sep)end + +--- Gives the character at the specified position +--- @param self string +--- @param i integer +--- @return string +function string.at(self, i)end + +--- string.format("%q", self) +--- @param self string +--- @return string +function string.quote(self)end + +--- Quotes the string, making it safe to pass to a shell script (unix only!!!) +--- @param self string +--- @return string +function string.quotesh(self)end diff --git a/addon/TAL/library/utils.lua b/addon/TAL/library/utils.lua new file mode 100644 index 0000000..3979bc0 --- /dev/null +++ b/addon/TAL/library/utils.lua @@ -0,0 +1,20 @@ +--- @meta + +box = table.pack; +unbox = table.unpack; +exit = os.exit; + +--- @param box table +--- @param s? number +--- @param e? number +--- @return table +function rebox(box, s, e) end + +--- Concatenates the arguments to a string +--- @return string +function str(...) end + +---@generic T +---@param obj { [integer]: T } +---@return fun(): T +function iterate(obj) end diff --git a/build/init.lua b/build/init.lua new file mode 100644 index 0000000..f8d14e9 --- /dev/null +++ b/build/init.lua @@ -0,0 +1,118 @@ +local fs; + +if not TAL then + local module = require "core.module"; + local root = module.init { + tal_path = { "./mod/" }, + lua_path = package.path, + target_glob = _G, + + pwd = "./", + home = "", + + join = require "mod.path".join, + cwd = require "mod.path".cwd, + }; + + require = root.require; + import = root.import; + export = root.export; + exports = root.exports; +end + +fs = require "fs"; + +local args = require "..mod.args"; +local modules = { + "core.entry", + "core.module", + "mod.path", + "mod.fs", +}; + +local function main(...) + local arg_readers = {}; + local target; + local executable; + local paths = array {}; + + function arg_readers.dst(v) + target = v; + return true; + end + function arg_readers.path(v) + paths:push(v); + return true; + end + function arg_readers.executable(v) + executable = v; + return true; + end + function arg_readers.linux() + target = fs.home() .. "/.local/bin/tal"; + + if fs.exists "/bin/luajit" then + executable = "/bin/luajit"; + elseif fs.exists "/bin/lua" then + executable = "/bin/lua" + else + print "No valid lua runtime found, WTF?"; + end + + paths = array { + "~/.local/lib/tal", + "~/.local/lib/.tal_mod", + "/usr/lib/tal", + "/usr/lib/.tal_mod", + "/usr/local/lib/.tal_mod", + }; + + return false; + end + + arg_readers.o = arg_readers.dst; + arg_readers.x = arg_readers.executable; + arg_readers.p = arg_readers.path; + + args(arg_readers, ...); + + if not target then error "No target specified" end + + local f = assert(io.open(target, "w")); + + if executable then + f:write(("#!%s\n"):format(executable)); + end + + f:write "load(string.dump(function(...)\n"; + f:write "function package.preload.__tal__PATH() return {\n"; + f:write(paths:map(function (v) return "\t" .. v:quote() .. ",\n" end, true):join""); + f:write("} end\n"); + + for i = 1, #modules do + local name = modules[i]; + local file = name:gsub("%.", "/") .. ".lua"; + + f:write(("package.preload[%q] = function(...)\n"):format(name)); + for el in assert(io.open(file, "r")):lines(1024) do + f:write(el); + end + f:write("\nend\n"); + end + + f:write(("require %q(...)"):format("core.entry")); + + f:write "\nend, true), '', 'b')(...)"; + + f:close(); + + if executable then + os.execute(("chmod +x %q"):format(target)); + end +end + +if type(module) ~= "table" then + main(...); +else + return main; +end diff --git a/build/lexer.lua b/build/lexer.lua new file mode 100644 index 0000000..db4133d --- /dev/null +++ b/build/lexer.lua @@ -0,0 +1,871 @@ +local TOK_ID = 1; +local TOK_OP = 2; +local TOK_STR = 3; +local TOK_NUM = 4; + +local operators = { + AND = 1, + OR = 2, + NOT = 3, + + CONCAT = 10, + ADD = 11, + SUB = 12, + MUL = 13, + DIV = 14, + IDIV = 15, + MOD = 16, + + B_AND = 20, + B_OR = 21, + B_XOR = 22, + B_LSH = 24, + B_RSH = 25, + RSH = 26, + + EQ = 30, + NEQ = 31, + LEQ = 32, + GEQ = 33, + LESS = 34, + GR = 35, + + PAREN_OPEN = 40, + PAREN_CLOSE = 41, + BRACKET_OPEN = 42, + BRACKET_CLOSE = 43, + BRACE_OPEN = 44, + BRACE_CLOSE = 45, + + SEMICOLON = 50, + COLON = 51, + COMMA = 52, + DOT = 53, + SPREAD = 54, + -- QUESTION = 55, + + ASSIGN = 60, + ASSIGN_OR = 75, +}; +local op_map = { + ["+"] = { operators.ADD }, + ["-"] = { operators.SUB }, + + ["*"] = { + operators.MUL, + ["*"] = { operators.POW }, + }, + ["/"] = { + operators.DIV, + ["/"] = { operators.IDIV }, + }, + ["%"] = { operators.MOD }, + + ["&"] = { operators.B_AND }, + ["|"] = {operators.B_OR }, + + ["^"] = { operators.POW }, + ["~"] = { + operators.B_XOR, + ["="] = { operators.NEQ }, + }, + + [">"] = { + operators.GR, + [">"] = { + operators.RSH, + [">"] = { operators.B_RSH }, + }, + }, + ["<"] = { + operators.LESS, + ["<"] = { operators.B_LSH }, + }, + + ["="] = { + operators.ASSIGN, + ["="] = { operators.EQ }, + }, + + [","] = { operators.COMMA }, + ["."] = { operators.DOT }, + [";"] = { operators.SEMICOLON }, + [":"] = { operators.COLON }, + ["?"] = { operators.QUESTION }, + + ["("] = { operators.PAREN_OPEN }, + [")"] = { operators.PAREN_CLOSE }, + ["["] = { operators.BRACKET_OPEN }, + ["]"] = { operators.BRACKET_CLOSE }, + ["{"] = { operators.BRACE_OPEN }, + ["}"] = { operators.BRACE_CLOSE }, +}; + +local to_byte = string.byte; + +--- @class tok_base +--- @field loc string +--- @field end_loc string +--- @field comments string[] +--- @field raw string + +--- @class tok_id: tok_base +--- @field type 1 +--- @field val string + +--- @class tok_op: tok_base +--- @field type 2 +--- @field val integer + +--- @class tok_str: tok_base +--- @field type 3 +--- @field val string + +--- @class tok_num: tok_base +--- @field type 4 +--- @field val number + +---@param base tok_base +---@param id string +---@return tok_id +local function tok_id(base, id) + base = base or { loc = "", comments = {} }; + return { + loc = base.loc, + comments = base.comments, + type = TOK_ID, + val = id, + } +end + +---@param base tok_base +---@param op integer +---@return tok_str +local function tok_op(base, op) + base = base or { loc = "", comments = {} }; + return { + loc = base.loc, + comments = base.comments, + type = TOK_OP, + val = op, + } +end + +---@param base tok_base? +---@param data string +---@return tok_str +local function tok_str(base, data) + base = base or { loc = "", comments = {} }; + return { + loc = base.loc, + comments = base.comments, + type = TOK_STR, + val = data, + }; +end + +---@param base tok_base +---@param num number +---@return tok_num +local function tok_num(base, num) + base = base or { loc = "", comments = {} }; + return { + loc = base.loc, + comments = base.comments, + type = TOK_NUM, + val = num, + }; +end + +--- @alias token +--- | tok_id +--- | tok_op +--- | tok_str +--- | tok_num + +---@param loader string | fun(): string +---@return fun(): string? +local function char_supplier(loader) + if type(loader) == "string" then + local i = 0; + + return function () + i = i + 1; + local res = string.sub(loader, i, i); + if #res == 1 then return res end + end + else + local curr_str; + local i = 0; + + return function () + if curr_str == "" then return nil end + + i = i + 1; + if curr_str == nil or i > #curr_str then + curr_str = loader(); + if curr_str == false or curr_str == nil or curr_str == "" then + curr_str = ""; + return nil; + end + i = 1; + end + + return string.sub(curr_str, i, i); + end + end +end + +---@param filename string +---@param chars fun(): string | nil +---@return fun(): token? +local function token_supplier(filename, chars, get_comments) + local line = 1; + local start = 1; + + local _chars = chars; + chars = function () + local c = _chars(); + if c == "\n" then + line = line + 1; + start = 1; + else + start = start + 1; + end + return c; + end + + local function unconsume(c) + if c == nil then return end + + local old_chars = chars; + + start = start - 1; + chars = function () + chars = old_chars; + start = start + 1; + return c; + end + end + + local consume_white; + + if get_comments then + local function consume_comment() + -- local data = {}; + -- local c = chars(); + + -- if c == "[" then + -- while true do + -- if c == nil then + -- return nil, "Unclosed comment"; + -- elseif c == "]" and chars() == "#" then + -- break; + -- end + -- data[#data + 1] = c; + -- c = chars(); + -- end + -- else + -- while true do + -- if c == "\n" or c == nil then break end + -- data[#data + 1] = c; + -- c = chars(); + -- end + -- end + + -- return table.concat(data); + + local data = array {}; + + local function singleline(c) + while true do + if c == "\n" or c == nil then break end + data:push(c); + c = chars(); + end + + return data:join ""; + end + local function multiline_end() + if chars() ~= "]" then return false end + if chars() ~= "-" then return false end + if chars() ~= "-" then return false end + + return true + end + local function multiline() + while true do + local c = chars() + if c == "]" and multiline_end() then + break; + elseif c == nil then + return nil, "Missing ]]"; + end + + data:push(c); + end + + return data:join ""; + end + + local c = chars() + + if c == "[" then + c = chars() + if c == "[" then + return multiline(); + else + return singleline(c); + end + else singleline(c) end + + return data:join ""; + end + ---@return false | string[]?, string? + function consume_white() + local comments = {}; + + while true do + local c = chars(); + + if c == nil then + chars = function () return nil end + break; + elseif start == 2 and line == 1 and c == "#" then + local c2 = chars(); + + if c2 == "!" then + while c ~= "\n" do + c = chars(); + end + else + unconsume(c2); + unconsume(c); + end + elseif c == "-" then + local c2 = chars() + + if c2 == "-" then + local res, err = consume_comment(); + if res == nil then return nil, err end + comments[#comments + 1] = res; + else + unconsume(c2) + unconsume(c) + break + end + elseif not string.find(c, "%s") then + unconsume(c); + break; + end + end + + return comments; + end + else + ---@return string? + local function consume_comment() + local c = chars(); + + if c == "[" then + while true do + c = chars(); + if c == nil then + return "Unclosed comment"; + elseif c == "]" and chars() == "#" then break end + end + else + while true do + if c == "\n" or c == nil then break end + c = chars(); + end + end + end + ---@return false | string[]?, string? + function consume_white() + while true do + local c = chars(); + + if c == nil then + chars = function () return nil end + break; + elseif c == "#" then + local err = consume_comment(); + if err ~= nil then return nil, err end + elseif not string.find(c, "%s") then + unconsume(c); + break; + end + end + + return false; + end + end + + local function hex_one(c) + local b = to_byte(c) + if b >= 48 and b <= 57 then + return b - 48 + elseif b >= 97 and b <= 102 then + return b - 97 + 10 + elseif b >= 65 and b <= 70 then + return b - 65 + 10 + else + return -1 + end + end + local function hex(base) + local res = 0 + local any = false + + while true do + local c = chars() + if c == nil then break end + + local digit = hex_one(c) + if digit == -1 then + unconsume(c) + break + else + res = res * 16 + digit + end + + any = true + end + + if not any then return end + + return tok_num(base, res) + end + local function decimal(res, float, mult) + local any = true + local fract_mult = .1 + + if type(res) == "string" then + local b = to_byte(res) + + if b >= 48 and b <= 57 then + res = b - 48 + else + return nil + end + elseif res == nil then + res, any = 0, false + end + + if mult == nil then mult = 1 end + + local c + + while true do + c = chars() + if c == nil then break end + + local b = to_byte(c) + if b >= 48 and b <= 57 then + any = true + if float then + res = res + (b - 48) * fract_mult + fract_mult = fract_mult * .1 + else + res = res * 10 + (b - 48) + end + else + break + end + end + + if any then + return mult * res, c + else + return nil, c + end + end + local function number(base, res) + local fract, e + local whole, next = decimal(res, false) + + if next == "." then + fract, next = decimal(nil, true) + end + if next == "e" then + local c = chars() + if c == "-" then + e, next = decimal(nil, false, -1) + else + e, next = decimal(c) + end + + if e == nil then + return nil, "Expected number after 'e'" + end + end + + if fract == nil then fract = 0 end + if e == nil then + e = 1 + else + e = 10 ^ e + end + + unconsume(next) + return tok_num(base, (whole + fract) * e) + end + local function zero(base) + local c = chars() + if c == nil then return tok_num(base, 0) end + + local b = to_byte(c) + + if c == "x" then + local res = hex(base) + if res == nil then return nil, "Expected a hex literal" + else return res end + else + unconsume(c) + return number(base, 0) + end + end + local function id(base, c) + local res = c + + while true do + c = chars() + if c == nil then break end + + local b = to_byte(c) + if + b >= 65 and b <= 90 or -- A-Z + b >= 97 and b <= 122 or -- a-z + b >= 48 and b <= 57 or -- 0-9 + c == "_" + then + res = res .. c + else + unconsume(c) + break + end + end + + base.raw = res + + return tok_id(base, res) + end + local function dot(base) + local e, fract, next = nil, decimal(nil, true) + + if fract == nil then + if next == "." then + local c = chars() + + if c == "." then + return tok_op(base, operators.SPREAD) + else + unconsume(c) + return tok_op(base, operators.CONCAT) + end + else + unconsume(next) + return tok_op(base, operators.DOT) + end + + return base + end + + if next == "e" then + local c = chars() + if c == "-" then + e, next = decimal(nil, false, -1) + else + e, next = decimal(c) + end + + if e == nil then + return nil, "Expected number after 'e'" + end + end + + if fract == nil then fract = 0 end + if e == nil then + e = 1 + else + e = 10 ^ e + end + + unconsume(next) + return tok_num(base, fract * e) + end + local function char(c, allow_newline) + if c == nil then return nil + elseif c == "\\" then + c = chars() + if c == "a" then return "\a" + elseif c == "b" then return "\b" + elseif c == "f" then return "\f" + elseif c == "n" then return "\n" + elseif c == "r" then return "\r" + elseif c == "t" then return "\t" + elseif c == "v" then return "\v" + elseif c == "z" then + repeat + c = chars() + until c == " " or c == "\n" or c == "\r" or c == "\t" or c == "\v" + return char(c) + elseif c == "x" then + local ca, cb = chars(), chars() + if ca == nil or cb == nil then return nil, "Expected a hex number" end + + local a, b = hex_one(ca), hex_one(cb) + if a == -1 or b == -1 then return nil, "Expected a hex number" end + + return string.char(a * 16 + b) + else return c end + else return c end + end + local function quote_str(base, first) + local res = {}; + + while true do + local c, err; + + c = chars() + if c == first then break end + if c == nil then return nil, "Unterminated string literal" end + + c, err = char(c) + if c == nil then + return nil, err or "Unterminated string literal"; + else + res[#res + 1] = c; + end + end + + return tok_str(base, table.concat(res)); + end + local function quote_char(base, first) + local res = 0; + + while true do + local c, err; + + c = chars(); + if c == first then break end + if c == nil then return nil, "Unterminated string literal" end + + c, err = char(c); + if c == nil then + return nil, err or "Unterminated string literal"; + else + for _, v in ipairs { string.byte(c, 1, #c) } do + res = res * 256 + v; + end + end + end + + return tok_num(base, res); + end + + return function () + -- local comments = consume_white() + local comments, err = consume_white(); + if comments == nil then + error(table.concat({ filename, line, start }, ":") .. ": " .. err); + elseif comments == false then + comments = nil; + end + + local loc = table.concat({ filename, line, start }, ":"); + --- @type table | nil + local base = { loc = loc, comments = comments, raw = "" }; + + local c = chars() + if c == nil then return nil end + local b = to_byte(c) + + if c == "." then base, err = dot(base) + elseif c == "0" then base, err = zero(base) + elseif b >= 49 and b <= 57 then base, err = number(base, b - 48) -- 1-9 + elseif + b >= 65 and b <= 90 or -- A-Z + b >= 97 and b <= 122 or -- a-z + c == "_" + then + base, err = id(base, c) + elseif c == "\"" then + base, err = quote_str(base, c); + elseif c == "\'" then + base, err = quote_char(base, c); + else + local res = op_map; + + while true do + local next = res[c]; + if next == nil then + unconsume(c); + res = res[1]; + break; + else + c = chars(); + res = next; + end + end + + if res == nil then + base, err = nil, string.format("Unexpected char '%s'", c) + else + base.type = TOK_OP; + base.val = res; + end + end + + if base == nil then + return error(loc .. ": " .. err); + else + base.end_loc = table.concat({ filename, line, start }, ":"); + return base; + end + end +end + +---@param ... fun(): token?, string? +local function concat_tokens(...) + local arr = {...}; + local i = 1; + + return function () + while true do + local el = arr[i]; + + if el == nil then + return nil; + elseif type(el) == "function" then + local buff = el(); + if buff ~= "" then + return buff; + else + i = i + 1; + end + end + end + end +end + +---@param first token +---@param second token +local function can_go_after(first, second) + if first.type == TOK_OP and second.type == TOK_OP then + if ( + first.val == operators.ASSIGN or + first.val == operators.EQ or + first.val == operators.LESS or + first.val == operators.GR or + first.val == operators.B_XOR + ) and ( + second.val == operators.ASSIGN or + second.val == operators.EQ + ) then return false end + if ( + first.val == operators.LESS or + first.val == operators.GR + ) and ( + second.val == operators.LESS or + second.val == operators.GR + ) then return false end + if ( + first.val == operators.DOT or + first.val == operators.CONCAT or + first.val == operators.SPREAD + ) and + ( + second.val == operators.DOT or + second.val == operators.CONCAT or + second.val == operators.SPREAD + ) then return false end + if ( + first.val == operators.LABEL or + first.val == operators.COLON + ) and ( + second.val == operators.LABEL or + second.val == operators.COLON + ) then return false end + if ( + first.val == operators.DIV or + first.val == operators.IDIV + ) and ( + second.val == operators.DIV or + second.val == operators.IDIV + ) then return false end + + return true; + elseif first.type == TOK_NUM and second.type == TOK_ID then + return false + elseif first.type == TOK_ID and second.type == TOK_NUM then + return false + elseif first.type == TOK_ID and second.type == TOK_ID then + return false + elseif first.type == TOK_NUM and second.type == TOK_OP then + return ( + second.val ~= operators.DOT and + second.val ~= operators.CONCAT and + second.val ~= operators.SPREAD + ); + elseif first.type == TOK_OP and second.type == TOK_NUM then + return ( + first.val ~= operators.DOT and + first.val ~= operators.CONCAT and + first.val ~= operators.SPREAD + ); + elseif first.type == TOK_ID and second.type == TOK_ID then + return true; + elseif first.type == TOK_NUM and second.type == TOK_NUM then + return false; + elseif first.type == TOK_STR and second.type == TOK_STR then + return false; + else + return true; + end +end + +---@param tokens fun(): token?, string? +---@return fun(): string?, string? +local function token_stringifier(tokens) + local last_tok + + return function () + if tokens == nil then return nil end + + local tok, err = tokens() + if tok == nil then + --- @diagnostic disable-next-line: cast-local-type + tokens = nil + return nil, err + end + + if last_tok ~= nil and not can_go_after(last_tok, tok) then + last_tok = tok + return " " .. tok.raw + else + last_tok = tok + return tok.raw + end + end +end + +return { + TOK_ID = TOK_ID, + TOK_OP = TOK_OP, + TOK_STR = TOK_STR, + TOK_NUM = TOK_NUM, + + operators = operators, + + char_supplier = char_supplier, + token_supplier = token_supplier, + concat_tokens = concat_tokens, + token_stringifier = token_stringifier, + + tok_id = tok_id, + tok_op = tok_op, + tok_num = tok_num, + tok_str = tok_str, +} diff --git a/build/require_filter.lua b/build/require_filter.lua new file mode 100644 index 0000000..587ab89 --- /dev/null +++ b/build/require_filter.lua @@ -0,0 +1,29 @@ +local lexer = require "lexer"; + +---@param tokens fun(): token?, string? +---@param mapper fun(name: string, base: tok_base): token? +---@return fun(): token?, string? +return function (tokens, mapper) + local last_req = false; + + return function () + local tok, err = tokens(); + + if tok == nil then + return tok, err; + elseif last_req then + if tok.type == lexer.TOK_STR then + last_req = false; + --- @diagnostic disable-next-line: param-type-mismatch + return mapper(tok.val, tok); + elseif tok.type ~= lexer.TOK_KW or tok.val ~= lexer.K_PAREN_OPEN then + last_req = false; + end + elseif tok.type == lexer.TOK_ID and tok.val == "require" then + last_req = true; + end + + return tok; + end +end + diff --git a/core/entry.lua b/core/entry.lua new file mode 100755 index 0000000..de330f1 --- /dev/null +++ b/core/entry.lua @@ -0,0 +1,17 @@ +local module = require "core.module"; +local fs = require "mod.fs"; +local path = require "mod.path"; + +TAL = "0.0.1"; + +return function (...) + local root, loop_run = module.init { + tal_path = require "__tal__PATH", + fs = fs, + path = path, + }; + + loop_run.run(function (...) + root.require "tal.cli".main(...); + end, ...); +end diff --git a/core/module.lua b/core/module.lua new file mode 100644 index 0000000..a4e7945 --- /dev/null +++ b/core/module.lua @@ -0,0 +1,324 @@ +local exports = {}; + +local function mk_module(name, init, parent) + local module; + local cache = {}; + + parent.modules = parent.modules or {}; + module = setmetatable({ + name = name, + exports = {}, + init = init, + env = setmetatable({}, { __index = parent.env }), + }, { __index = parent }); + + module.returns = { n = 1, module.exports }; + + function module.resolve(id, list) + if cache[id] ~= nil then return cache[id] end + + for i = 1, #module.resolvers do + local res, files = module.resolvers[i](module, id); + if res then + cache[id] = res; + return res; + elseif list then + for i = 1, #files do + list[#list + 1] = files[i]; + end + end + end + + return nil; + end + function module.import(id) + local paths = {}; + local mod = module.resolve(id, paths); + if mod == nil then + error(table.concat { + "Unable to resolve module '", + id, + "' - tried the following paths:\n- ", + table.concat(paths, "\n- ") + }, 2); + elseif type(mod.init) == "function" then + local init = mod.init; + mod.init = nil; + init(mod); + end + + return (unpack or table.unpack)(mod.returns, 1, mod.returns.n); + end + + function module.require(id) + local original_id = id; + local match = id:match "[^/]+$"; + + if match == id then + local start = id:match "^%.+" or ""; + local rest = id:sub(#start + 1):gsub("%.", "/"); + + if #start == 0 then + id = rest; + elseif #start == 1 then + id = "./" .. rest; + else + id = ("../"):rep(#start - 1) .. rest; + end + else + id = id:sub(1, -#match - 1) .. match:gsub("%.", "/"); + end + + local paths = {}; + local mod = module.resolve(id .. ".lua", paths); + + if mod == nil then + mod = module.resolve(id .. "/init.lua", paths); + end + + if mod == nil then + error(table.concat { + "Unable to resolve module '", + original_id, + "' - tried the following paths:\n- ", + table.concat(paths, "\n- ") + }, 2); + elseif type(mod.init) == "function" then + local init = mod.init; + mod.init = nil; + init(mod); + end + + return (unpack or table.unpack)(mod.returns, 1, mod.returns.n); + end + function module.export(exports) + for k, v in next, exports do + module.exports[k] = v; + end + end + function module.mk(name, init) + if module.modules[name] ~= nil then + return module.modules[name], true; + else + local mod = mk_module(name, init, module); + module.modules[name] = mod; + return mod, false; + end + end + + module.env.module = module; + module.env.import = module.import; + module.env.require = module.require; + module.env.export = module.export; + module.env.exports = module.exports; + + module.env.package = setmetatable({}, { + __index = function() + error "This is a TAL module, please use 'module' instead"; + end, + __newindex = function() + error "This is a TAL module, please use 'module' instead"; + end, + }); + + return module; +end + +local function try_file(self, cwd, id, base, suffixes) + suffixes = suffixes or { "" }; + local file, found; + local files = {}; + + for i = 1, #suffixes do + file = cwd(base, id .. suffixes[i]); + + local f = io.open(file, "r"); + if f ~= nil then + f:close(); + found = true; + break; + end + + files[#files + 1] = file; + end + + if not found then + return nil, files; + end + + return self.mk(file, function (self) + local func = assert(loadfile(file, "t", self.env)); + + local function fin(...) + if select("#", ...) > 0 then + self.exports = ...; + self.returns = { n = select("#", ...), ... }; + end + end + fin(func()); + end), file; +end + +function exports.file_resolver(cwd, join) + return function(self, id) + if id:match "^%./" or id:match "^%.%./" then + return try_file(self, cwd, id, join(self.name, "..", self.extensions), self.extensions); + else + local files = {}; + + for i = 1, #self.paths do + local res, file = try_file(self, cwd, id, self.paths[i], self.extensions); + if res ~= nil then + return res, file; + end + + for i = 1, #file do + files[#files + 1] = file[i]; + end + end + + return nil, files; + end + end +end +function exports.lua_resolver(opts) + local returns = { + modules = {}, + }; + + function returns.resolver(self, id) + if id:match "^lua:" then + id = id:sub(5); + end + + if returns.modules[id] ~= nil then + return returns.modules[id], "lua:" ..id; + else + return nil, { "lua:" .. id }; + end + end + + function returns.register(name, ...) + returns.modules[name] = { + name = "lua:" .. name, + exports = ..., + returns = { n = select("#", ...), ... }, + }; + end + + if opts.glob then + returns.register("global.lua", opts.glob); + end + + if opts.expose then + local expose = opts.expose; + if expose == true then + expose = { + "utf8", "bit32", "bit", + "table", "string", "os", "math", "jit", "io", "ffi", "debug", "coroutine", + "table.new", "table.clear", + "string.buffer", + "jit.profile", + }; + end + + for i = 1, #expose do + local ok, mod = pcall(require, expose[i]); + if ok then + returns.register(expose[i]:gsub("%.", "/") .. ".lua", mod); + end + end + end + + if opts.root then + returns.register("root.lua", opts.root); + end + + returns.register("lua-resolver.lua", returns); + + return returns; +end + +function exports.mk_root(opts) + local run_loop; + local root = mk_module( + opts.join(opts.pwd, "/"), + nil, + { + paths = {}, + resolvers = {}, + modules = {}, + env = opts.glob, + } + ); + + root.paths[#root.paths + 1] = opts.pwd .. "/.tal_mod"; + + if opts.tal_path then + for i = 1, #opts.tal_path do + local p = opts.tal_path[i]; + + if p:match "^~/" then + root.paths[#root.paths + 1] = opts.join(opts.home, p:sub(2)); + elseif p:match "^/" then + root.paths[#root.paths + 1] = opts.join(p); + else + root.paths[#root.paths + 1] = opts.join(opts.pwd, p); + end + end + end + + if opts.lua_path then + for v in opts.lua_path:gmatch "[^;]+" do + local match = v:match "^(.+)/%?%.lua$"; + if match ~= nil and match ~= "." then + root.paths[#root.paths + 1] = match; + end + end + end + + if opts.resolvers then + root.resolvers[#root.resolvers + 1] = exports.file_resolver(opts.cwd, opts.join); + root.resolvers[#root.resolvers + 1] = exports.lua_resolver { + glob = opts.glob, + expose = true, + root = root, + }.resolver; + end + + return root, run_loop; +end + +function exports.init(opts, msg) + if opts.glob == nil then + ---@diagnostic disable-next-line: deprecated + opts.glob = _ENV or _G or (getfenv and getfenv()); + if opts.glob == nil then + error "Environment is not accessible"; + end + end + + local root = exports.mk_root { + tal_path = opts.tal_path, + lua_path = opts.lua_path or package.path, + resolvers = true, + evn_loop = true, + + pwd = opts.pwd or opts.fs.pwd(), + home = opts.home or opts.fs.home(), + + join = opts.join or opts.path.join, + cwd = opts.cwd or opts.path.cwd, + glob = opts.glob, + }; + + local require = root.require; + + require "core"(opts.target_glob or root.env); + local run_loop = require "core.evn-loop"; + root.env.promise = require "promise"; + + return root, run_loop; +end + +return exports; diff --git a/mod/args.lua b/mod/args.lua new file mode 100644 index 0000000..d8e2d4a --- /dev/null +++ b/mod/args.lua @@ -0,0 +1,106 @@ +---@param consumers table +--- @param ... string +--- @returns nil +return function (consumers, ...) + local consumer_stack = array {}; + local pass_args = false; + + local function digest_arg(v) + local consumed = false; + + while true do + local el = consumer_stack:pop(); + if el == nil then break end + + local res, fin = el(v); + if res then + consumed = true; + end + if fin then + pass_args = true; + break; + end + + if fin or res then + break; + end + end + + if not consumed then + if consumers[1](v) then + pass_args = true; + end + end + end + + local function get_consumer(name) + local consumer = name; + local path = {}; + local n = 0; + + while true do + local curr = consumer; + if path[curr] then + local path_arr = array {}; + + for k, v in next, path do + path_arr[v] = k; + end + + error("Alias to '" .. curr .. "' is recursive: " .. path_arr:join " -> "); + end + + consumer = consumers[curr]; + if consumer == nil then + local prefix; + if n == 0 then + prefix = "Unknown flag"; + else + prefix = "Unknown alias"; + end + + if #curr == 1 then + error(prefix .. " '-" .. curr .. "'"); + else + error(prefix .. " '--" .. curr .. "'"); + end + elseif type(consumer) == "function" then + return consumer; + end + + path[curr] = n; + n = n + 1; + end + end + + local function next(...) + if ... == nil then + while #consumer_stack > 0 do + local el = consumer_stack:pop(); + + if el(nil) then + error("Unexpected end of arguments", 0); + break; + end + end + else + if pass_args then + consumers[1]((...)); + elseif ... == "--" then + pass_args = true; + elseif (...):match "^%-%-" then + consumer_stack:push(get_consumer((...):sub(3))); + elseif (...):match "^%-" then + for c in (...):sub(2):gmatch "." do + consumer_stack:unshift(get_consumer(c)); + end + else + digest_arg(...); + end + + return next(select(2, ...)); + end + end + + return next(...); +end diff --git a/mod/core/array.lua b/mod/core/array.lua new file mode 100644 index 0000000..4943512 --- /dev/null +++ b/mod/core/array.lua @@ -0,0 +1,183 @@ +return function (glob) + --- @diagnostic disable: duplicate-set-field + + --- @class arraylib + local arrays = {}; + arrays.__index = arrays; + + local function array(obj) + if type(obj) == "string" then + return obj:split ""; + else + return arrays.mk(obj); + end + end + --- LuaLS is a piece of shit + --- @generic T + --- @param type `T` + --- @return fun(obj: T[]): array + local function arrof(type) + return array; + end + + --- Creates an array + function arrays.mk(obj) + return setmetatable(obj, arrays); + end + + function arrays.concat(...) + --- @diagnostic disable-next-line: missing-fields + return arrays.append(array {}, ...); + end + + function arrays:append(...) + local res = self; + local n = #res + 1; + + for i = 1, select("#", ...) do + local curr = select(i, ...); + for j = 1, #curr do + res[n] = curr[j]; + n = n + 1; + end + end + + return res; + end + + function arrays:push(...) + return self:append { ... }; + end + function arrays:pop() + local res = self[#self]; + self[#self] = nil; + return res; + end + function arrays:peek() + return self[#self]; + end + + function arrays:shift() + return table.remove(self, 1); + end + function arrays:unshift(...) + local len = select("#", ...); + table.move(self, 1, #self, len + 1, self); + for i = 1, len do + self[i] = select(i, ...); + end + end + + function arrays:map(f, mutate) + local out; + + if mutate then + out = self; + else + out = array {}; + end + + for i = 1, #self do + out[i] = f(self[i], i, self); + end + + return out; + end + function arrays:flat_map(f) + local out = array {}; + + for i = 1, #self do + out:append(f(self[i], i, self)); + end + + return out; + end + function arrays:each(f) + for i = 1, #self do + f(self[i], i, self); + end + end + function arrays:sort(f, copy) + local target = self; + if copy then + target = array {}:append(self); + end + + table.sort(target, f); + return target; + end + + function arrays:find_i(f) + for i = 1, #self do + if f(self[i], i, self) then + return i; + end + end + + return nil; + end + + + function arrays:fill(val, b, e) + if b == nil then b = 1 end + if e == nil then e = self end + if b < 0 then b = #self + 1 - b end + if e < 0 then e = #self + 1 - e end + + for i = b, e do + self[i] = val; + end + + return self; + end + + function arrays:splice(b, e, ...) + -- TODO: optimize + if select("#") > 0 then + local n = e - b + 1; + + while n > 0 do + table.remove(self, b); + n = n - 1; + end + + for i = 1, select("#") do + table.insert(self, b, (select(i, ...))); + end + + return self; + else + local res = {}; + + for i = b, e do + table.insert(res, self[i]); + end + + return res; + end + end + + function arrays:slice(b, e) + b = b or 1; + e = e or #self; + local res = array {}; + + for i = b, e do + res:push(self[i]); + end + + return res; + end + + function arrays:join(sep, b, e) + return table.concat(self, sep, b, e); + end + + function arrays:__concat(other) + return arrays.concat(self, other); + end + + glob.arrays = arrays; + glob.array = array; + glob.arrof = arrof; +end diff --git a/mod/core/coro.lua b/mod/core/coro.lua new file mode 100644 index 0000000..8204ea0 --- /dev/null +++ b/mod/core/coro.lua @@ -0,0 +1,234 @@ +-- symmetric coroutines from the paper at +-- http://www.inf.puc-rio.br/~roberto/docs/corosblp.pdf +-- Written by Cosmin Apreutesei. Public Domain. + +-- Reworked from the (in)famous coro lib +---@diagnostic disable: duplicate-set-field + +local old_create = coroutine.create; +local old_close = coroutine.close; +local old_running = coroutine.running; +local old_status = coroutine.status; +local old_resume = coroutine.resume; +local old_yield = coroutine.yield; +--- @diagnostic disable-next-line: deprecated + +local main, is_main = coroutine.running(); +if main ~= nil and not is_main then + error "This library must be initialized in the main thread"; +elseif main == nil then + main = old_create(function () end); + old_resume(main); +end + +local threads = setmetatable({}, { __mode = "k" }); +local resumers = setmetatable({}, { __mode = "k" }); +local responsible = setmetatable({}, { __mode = "k" }); + +local current = main; + +threads[main] = "main"; + +local function assert_thread(thread, level) + if type(thread) ~= "thread" then + local err = string.format("coroutine expected but %s given", type(thread)); + error(err, level); + end + + return thread; +end + +local function unprotect(thread, ok, ...) + if not ok then + local s = debug.traceback(thread, (...)); + s = string.gsub(s, "stack traceback:", tostring(thread) .. " stack traceback:"); + error(s, 2); + end + return ...; +end + +local function finish(thread, ...) + local caller = resumers[thread]; + if not caller then + error("coroutine ended without transferring control", 4); + end + return caller, true, ...; +end + +local function go(thread, arg_box) + while true do + current = thread + if thread == main then + -- transfer to the main thread: stop the scheduler. + return unbox(arg_box); + end + + -- transfer to a coroutine: resume it and check the result. + arg_box = box(old_resume(thread, unbox(arg_box))); + + if not arg_box[1] then + -- the coroutine finished with an error. pass the error back to the + -- caller thread, or to the main thread if there's no caller thread. + thread = resumers[thread] or main; + arg_box = box(arg_box[1], arg_box[2], debug.traceback()); + else + -- loop over the next transfer request. + thread = arg_box[2]; + arg_box = rebox(arg_box, 3); + end + end +end +local coro = {}; + +function coro.create(f) + local thread; + thread = old_create(function(ok, ...) + return finish(thread, f(...)); + end); + responsible[thread] = current; + return thread; +end + +function coro.running() + return current, current == main; +end + +function coro.status(thread) + assert_thread(thread, 2); + return old_status(thread); +end + +function coro.ptransfer(thread, ...) + assert(thread ~= current, "trying to transfer to the running thread"); + + if current ~= main then + -- we're inside a coroutine: signal the transfer request by yielding. + return old_yield(thread, true, ...); + else + -- we're in the main thread: start the scheduler. + local arg_box = box(true, ...); + + while true do + current = thread; + + if thread == main then + -- transfer to the main thread: stop the scheduler. + return unbox(arg_box); + end + + -- transfer to a coroutine: resume it and check the result. + arg_box = box(old_resume(thread, unbox(arg_box))); + + if not arg_box[1] then + -- the coroutine finished with an error. pass the error back to the + -- caller thread, or to the main thread if there's no caller thread. + thread = responsible[thread] or main; + arg_box = box(arg_box[1], arg_box[2], debug.traceback()); + else + -- loop over the next transfer request. + thread = arg_box[2]; + arg_box = rebox(arg_box, 3); + end + end + end +end + +function coro.transfer(thread, ...) + -- print(current, ">", thread, ...); + return unprotect(thread, coro.ptransfer(thread, ...)); +end + +function coro.wrap(f) + local calling_thread, yielding_thread; + local function yield(...) + yielding_thread = current; + return coro.transfer(calling_thread, ...); + end + local function finish(...) + yielding_thread = nil; + return coro.transfer(calling_thread, ...); + end + local function wrapper(...) + return finish(f(yield, ...)); + end + + local thread = coro.create(wrapper); + yielding_thread = thread; + + return function(...) + resumers[thread] = calling_thread; + calling_thread = current; + assert(yielding_thread, "cannot transfer to dead coroutine"); + return coro.transfer(yielding_thread, ...); + end, thread; +end + +--- @generic T, T1, T2, T3, T4, T5, Args +--- @param f fun(yield: (fun(p1?: T1, p2?: T2, p3?: T3, p4?: T4, p5?: T5): ...), ...: Args) +--- @return fun(...: Args): fun(...): T1, T2, T3, T4, T5 +function coro.gen(f) + return function (...) + local prev = box(...); + + return coro.wrap(function (yield) + return f(yield, unbox(prev)); + end); + end +end + +function coro.close(t) + if old_close == nil then + return false, "Closing coroutines is not supported"; + else + return old_close(t); + end +end + +local coroutine = {}; + +function coroutine.close(co) + coro.close(co); +end +function coroutine.create(f) + return coro.create(f); +end + +function coroutine.isyieldable() + return true; +end + +function coroutine.resume(co, ...) + resumers[co] = coro.running(); + responsible[co] = coro.running(); + return coro.ptransfer(co, ...); +end + +function coroutine.yield(...) + local cb = resumers[coro.running()]; + if cb == nil then + error("Must call 'yield' from a thread that has been invoked asymmetrically", 2); + end + + return coro.transfer(cb, ...); +end + +function coroutine.wrap(f) + local co = coroutine.create(f); + + return function (...) + return assert(coroutine.resume(co, ...)); + end +end + +function coroutine.running() + return coro.running(); +end + +function coroutine.status(co) + return coro.status(co); +end + +return function (glob) + glob.coroutine = coroutine; + glob.coro = coro; +end diff --git a/mod/core/env.lua b/mod/core/env.lua new file mode 100644 index 0000000..868eedd --- /dev/null +++ b/mod/core/env.lua @@ -0,0 +1,44 @@ +local env = {}; + +local has_jit, jit = pcall(require, "jit"); + +if has_jit then + env.jit = true; + env.os = jit.os; + env.runtime = jit.version; + env.arch = jit.arch; +else + env.jit = false; + print "NOT RUNNING IN LUAJIT!!"; + print "Some libraries might depend on ffi. Continue at your own risk!"; + + env.jit = false; + env.os = "unknown"; + env.arch = "unknown"; + + local function extract_os() + if not io.popen then return end + + local uname_f = io.popen("uname -ms"); + if uname_f ~= nil then + local os, arch = uname_f:read "*a":sub(1, -2):match "([^%s]+) ([^%s]+)"; + + env.os = os; + env.arch = arch; + return; + end + + local os_name = os.getenv "OS"; + if os_name == "Windows_NT" then + env.os = "windows"; + env.arch = os.getenv "PROCESSOR_ARCHITECTURE"; + end + end + + extract_os(); + env.runtime = _VERSION; +end + +return function (glob) + glob.env = env; +end diff --git a/mod/core/evn-loop.lua b/mod/core/evn-loop.lua new file mode 100644 index 0000000..b2de2a6 --- /dev/null +++ b/mod/core/evn-loop.lua @@ -0,0 +1,55 @@ +local resolver = require "lua-resolver"; + +local stack = array {}; +local active; + +local exports = {}; + +--- @param ... fun() +function exports.push(...) + stack:push(...); +end + +function exports.interrupt() + if coro.running() == active then + -- We are trying to interrupt the currently active event loop thread + -- We will transfer away from it and "abandon" it (leave it for non-event loop use) + return coro.transfer(exports.main); + else + -- We are currently in an abandoned or a non-loop thread. We can calmly transfer to the active thread + return coro.transfer(active); + end +end + +--- @param main fun(...) +--- @param ... any +function exports.run(main, ...) + if active then + error "Event loop is already running!"; + end + exports.main = coro.running(); + + local args = box(...); + exports.push(function () + main(unbox(args)); + end) + + while #stack > 0 do + active = coro.create(function () + while #stack > 0 and coro.running() == active do + local msg = stack:shift(); + msg(); + end + + coro.transfer(exports.main); + end); + + coro.transfer(active); + print "Abandoned thread..."; + end +end + +-- Register the module as phony +resolver.register("evn-loop.lua", exports); + +return exports; diff --git a/mod/core/function.lua b/mod/core/function.lua new file mode 100644 index 0000000..c87c1f5 --- /dev/null +++ b/mod/core/function.lua @@ -0,0 +1,57 @@ +local unpack = unpack or table.unpack; + +--- @class functions +local functions = {}; +functions.__index = functions; + +--- Constructs a function, such that it calls the first function with the passed arguments, +--- the second function with the return of the first, and so on. The return value of the last function is returned +--- +--- In short, does the following, if the passed functions are a, b and c: return c(b(a(...))) +--- +--- Sometimes less cumbersome to write (a | b | c | d)(args...) than d(c(b(a(args...)))) +--- @param self function +function functions:pipe(...) + if ... == nil then + return self; + else + local next = ...; + + return functions.pipe(function (...) + return next(self(...)); + end, select(2, ...)); + end +end +function functions:apply(args) + return self(unpack(args)); +end +--- Constructs a function, such that it calls the first function with the passed arguments, +--- the second function with the return of the first, and so on. The return value of the last function is returned +--- +--- In short, does the following, if the passed functions are a, b and c: return c(b(a(...))) +--- +--- Sometimes less cumbersome to write (a | b | c | d)(args...) than d(c(b(a(args...)))) +--- @param self function +function functions:pcall(...) + return pcall(self, ...); +end +--- Calls pipe with a and b; Alternative syntax for older Lua installation +function functions.__sub(a, b) + return functions.pipe(a, b); +end +--- Calls pipe with a and b +function functions.__bor(a, b) + return functions.pipe(a, b); +end + + +return function (glob) + -- It's not vital to have this metatable, so we will just try our very best + if debug then + debug.setmetatable(debug.setmetatable, functions); + end + + glob.functions = functions; + glob["function"] = functions; +end + diff --git a/mod/core/init.lua b/mod/core/init.lua new file mode 100644 index 0000000..eb7781e --- /dev/null +++ b/mod/core/init.lua @@ -0,0 +1,10 @@ +return function (glob) + require ".polyfills"(glob); + require ".array"(glob); + require ".coro"(glob); + require ".env"(glob); + require ".function"(glob); + require ".printing"(glob); + require ".string"(glob); + require ".utils"(glob); +end diff --git a/mod/core/polyfills.lua b/mod/core/polyfills.lua new file mode 100644 index 0000000..213d8bb --- /dev/null +++ b/mod/core/polyfills.lua @@ -0,0 +1,72 @@ +return function (glob) + if glob.getfenv then + glob._ENV = glob.getfenv(); + glob._G = _ENV; + else + glob._G = _ENV; + + if debug.getupvalue then + local getupvalue = debug.getupvalue; + function glob.getfenv(func) + return getupvalue(func, 1); + end + else + function glob.getfenv() + error("getfenv is not supported", 2); + end + end + end + + if not glob.setfenv then + if glob.debug.setupvalue then + local setupvalue = glob.debug.setupvalue; + + if glob.debug.getinfo then + local getinfo = glob.debug.getinfo; + + function glob.setfenv(func, val) + setupvalue(func, 1, val); + if type(func) == "function" then + return func; + else + return getinfo(func, "f"); + end + end + else + function glob.setfenv(func, val) + setupvalue(func, 1, val); + return func; + end + end + else + function glob.setfenv() + error("setfenv is not supported", 2); + end + end + end + + -- Reimplement this for lua <5.1 + if glob.table.move == nil then + --- @diagnostic disable: duplicate-set-field + function glob.table.move(src, src_b, src_e, dst_b, dst) + if dst == nil then dst = src end + local offset = dst_b - src_b; + + if dst_b < src_b then + for i = src_e, src_b, -1 do + dst[i + offset] = src[i]; + end + else + for i = src_b, src_e do + dst[i + offset] = src[i]; + end + end + + return dst; + end + end + + -- Why did we *remove* this from the spec again? Classical PUC + glob.unpack = table.unpack or unpack; + glob.table.unpack = unpack; +end diff --git a/mod/core/printing.lua b/mod/core/printing.lua new file mode 100644 index 0000000..a167a1e --- /dev/null +++ b/mod/core/printing.lua @@ -0,0 +1,141 @@ +local function escape_str(s) + local in_char = { "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" }; + local out_char = { "\\", "\"", "/", "b", "f", "n", "r", "t" }; + for i, c in ipairs(in_char) do + s = s:gsub(c, "\\" .. out_char[i]); + end + return s +end + +local function stringify_impl(obj, indent_str, n, passed) + local parts = {}; + local kind = type(obj); + + if kind == "table" then + if passed[obj] then return "" end + passed[obj] = true; + + local len = #obj; + + for i = 1, len do + parts[i] = stringify_impl(obj[i], indent_str, n + 1, passed) .. ","; + end + + local keys = {}; + + for k, v in pairs(obj) do + if type(k) ~= "number" or k > len then + keys[#keys + 1] = { k, v }; + end + end + + table.sort(keys, function (a, b) + if type(a[1]) == "number" and type(b[1]) == "number" then + return a[1] < b[1]; + elseif type(a[1]) == "string" and type(b[1]) == "string" then + return a[1] < b[1]; + else + return type(a[1]) < type(b[1]) or tostring(a[1]) < tostring(b[1]); + end + end); + + for i = 1, #keys do + local k = keys[i][1]; + local v = keys[i][2]; + + local val = stringify_impl(v, indent_str, n + 1, passed); + if val ~= nil then + if type(k) == "string" then + parts[#parts + 1] = table.concat { k, ": ", val, "," }; + else + parts[#parts + 1] = table.concat { "[", stringify_impl(k, indent_str, n + 1, passed), "]: ", val, "," }; + end + end + end + + local meta = getmetatable(obj); + if meta ~= nil and meta ~= arrays then + parts[#parts + 1] = " = " .. stringify_impl(meta, indent_str, n + 1, passed) .. ","; + end + + passed[obj] = false; + + if #parts == 0 then + if meta == arrays then + return "[]"; + else + return "{}"; + end + end + + local contents = table.concat(parts, " "):sub(1, -2); + if #contents > 80 then + local indent = "\n" .. string.rep(indent_str, n + 1); + + contents = table.concat { + indent, + table.concat(parts, indent), + "\n", string.rep(indent_str, n) + }; + else + contents = " " .. contents .. " "; + end + + if meta == arrays then + return table.concat { "[", contents, "]" }; + else + return table.concat { "{", contents, "}" }; + end + elseif kind == "string" then + return "\"" .. escape_str(obj) .. "\""; + elseif kind == "function" then + local data = debug.getinfo(obj, "S"); + return table.concat { tostring(obj), " @ ", data.short_src, ":", data.linedefined }; + elseif kind == "nil" then + return "nil"; + else + return tostring(obj); + end +end + +--- Turns the given value to a human-readable string. +--- Should be used only for debugging and display purposes +local function to_readable(obj, indent_str) + return stringify_impl(obj, indent_str or " ", 0, {}); +end + +local function print(...) + for i = 1, select("#", ...) do + if i > 1 then + io.stderr:write("\t"); + end + + io.stderr:write(tostring((select(i, ...)))); + end + + io.stderr:write("\n"); +end + +--- Prints the given values in a human-readable manner to stderr +--- Should be used only for debugging +local function pprint(...) + if select("#", ...) == 0 then + io.stderr:write "\n"; + else + for i = 1, select("#", ...) do + if i > 1 then + io.stderr:write "\t"; + end + + io.stderr:write(to_readable((select(i, ...)))); + end + + io.stderr:write "\n"; + end +end + +return function (glob) + glob.to_readable = to_readable; + glob.print = print; + glob.pprint = pprint; +end diff --git a/mod/core/string.lua b/mod/core/string.lua new file mode 100644 index 0000000..47d5a02 --- /dev/null +++ b/mod/core/string.lua @@ -0,0 +1,70 @@ +--- @diagnostic disable: duplicate-set-field + +return function (glob) + --- @param self string + function glob.string:split(sep) + sep = sep or ""; + local lines = array {}; + local pos = 1; + + if sep == "" then + for i = 1, #self do + lines:push(self:sub(1, 1)); + end + else + while true do + local b, e = self:find(sep, pos); + + if not b then + table.insert(lines, self:sub(pos)); + break; + else + table.insert(lines, self:sub(pos, b - 1)); + pos = e + 1; + end + end + end + + return lines; + end + --- @param self string + function glob.string:at(i) + return self:sub(i, i); + end + --- @param self string + function glob.string:replace_first(old, new) + local b, e = self:find(old, 1, true); + + if b == nil then + return self; + else + return self:sub(1, b - 1) .. new .. self:sub(e + 1); + end + end + + --- @param self string + function glob.string:quote() + return ("%q"):format(self); + end + + --- @param self string + function glob.string:quotesh() + return "'" .. self + :gsub("*", "\\*") + :gsub("?", "\\?") + :gsub("~", "\\~") + :gsub("$", "\\$") + :gsub("&", "\\&") + :gsub("|", "\\|") + :gsub(";", "\\;") + :gsub("<", "\\<") + :gsub(">", "\\>") + :gsub("(", "\\)") + :gsub("[", "\\]") + :gsub("{", "\\}") + :gsub("%\\", "\\\\") + :gsub("\'", "\\\'") + :gsub("\"", "\\\"") + :gsub("`", "\\`") .. "'"; + end +end diff --git a/mod/core/utils.lua b/mod/core/utils.lua new file mode 100644 index 0000000..a37c972 --- /dev/null +++ b/mod/core/utils.lua @@ -0,0 +1,72 @@ +return function (glob) + function glob.box(...) + return { n = select("#", ...), ... }; + end + function glob.unbox(obj, s, e) + return unpack(obj, s or 1, e or obj.n); + end + function glob.rebox(obj, s, e) + return box(unpack(obj, s or 1, e or obj.n)); + end + + function glob.exit() + os.exit(0); + end + + function glob.str(...) + return table.concat { ... }; + end + + function glob.iterate(obj) + local i = 0; + + return function() + i = i + 1; + return obj[i]; + end + end + + --- Prepares the object to be a class - puts an __index member in it pointing to the object itself + --- @generic T + --- @param obj T + --- @return T | { __index: T } + function glob.class(obj) + --- @diagnostic disable-next-line: inject-field + obj.__index = obj; + return obj; + end + + function glob.try(try_fn, catch_fn, finally_fn) + if not try_fn then + if finally_fn then + return finally_fn(); + end + else + local function handle_catch(...) + if finally_fn then + finally_fn(); + end + + return ...; + end + + local function handle_try(ok, ...) + if ok then + if finally_fn then + finally_fn(); + end + + return ...; + else + if catch_fn then + return handle_catch(catch_fn(...)); + elseif finally_fn then + return finally_fn(); + end + end + end + + return handle_try(pcall(try_fn)); + end + end +end diff --git a/mod/fs.lua b/mod/fs.lua new file mode 100644 index 0000000..4ed0925 --- /dev/null +++ b/mod/fs.lua @@ -0,0 +1,23 @@ +local exports = {}; + +function exports.exists(path) + local f = io.open(path, "r"); + if f == nil then return false end + if f:read(1) == nil then return false end + + return true; +end + +function exports.home() + return os.getenv "HOME"; +end + +function exports.pwd() + return os.getenv "PWD" or "./"; +end + +function exports.ls(path) + return os.execute("ls ") +end + +return exports; diff --git a/mod/json.lua b/mod/json.lua new file mode 100644 index 0000000..d328914 --- /dev/null +++ b/mod/json.lua @@ -0,0 +1,245 @@ +local exports = {}; + +-- We can have arbitrary promises in the middle of the module +-- NO MORE ASYNC AWAIT MADNESS + +promise.resolve():await(); + +local function kind_of(obj) + if type(obj) ~= "table" then return type(obj) end + + local i = 1; + for _ in pairs(obj) do + if obj[i] ~= nil then + i = i + 1; + else + return "table"; + end + end + + if i == 1 then + return "table"; + else + return "array"; + end +end + +local function escape_str(s) + local in_char = { "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" }; + local out_char = { "\\", "\"", "/", "b", "f", "n", "r", "t" }; + for i, c in ipairs(in_char) do + s = s:gsub(c, "\\" .. out_char[i]); + end + return s +end + +-- Returns pos, did_find; there are two cases: +-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. +-- 2. Delimiter not found: pos = pos after leading space; did_find = false. +-- This throws an error if err_if_missing is true and the delim is not found. +local function skip_delim(str, pos, delim, err_if_missing) + pos = string.find(str, "%S", pos) or pos; + + if string.sub(str, pos, pos) ~= delim then + if err_if_missing then + error(table.concat { "Expected ", delim, " near position ", pos }); + end + + return pos, false; + end + + return pos + 1, true; +end + +local esc_map = { b = "\b", f = "\f", n = "\n", r = "\r", t = "\t", v = "\v" }; + +-- Expects the given pos to be the first character after the opening quote. +-- Returns val, pos; the returned pos is after the closing quote character. +local function parse_str_val(str, pos, check) + if pos > #str then error("End of input found while parsing string") end + + if check then + if string.sub(str, pos, pos) ~= "\"" then + return nil, pos; + else + pos = pos + 1; + end + else + pos = pos + 1; + end + + local res = {}; + + while true do + local c = string.sub(str, pos, pos); + if c == "\"" then + return table.concat(res), pos + 1; + elseif c == "\\" then + c = string.sub(str, pos + 1, pos + 1); + res[#res + 1] = esc_map[c] or c; + pos = pos + 2; + else + res[#res + 1] = c; + pos = pos + 1; + end + end +end + +-- Returns val, pos; the returned pos is after the number's final character. +local function parse_num_val(str, pos) + local num_str = string.match(str, "^-?%d+%.?%d*[eE]?[+-]?%d*", pos); + local val = tonumber(num_str); + + if not val then error(table.concat { "Error parsing number at position ", pos, "." }) end + return val, pos + #num_str; +end + +local json_end = { "eof" }; + +local function parse_impl(str, pos, end_delim) + pos = pos or 1; + if pos > #str then error("Reached unexpected end of input") end + + pos = str:find("%S", pos) or pos; + local c = str:sub(pos, pos); + local delim_found; + + if c == "{" then + pos = pos + 1; + + local key; + local obj = {}; + + c = string.sub(str, pos, pos); + if c == "}" then + return obj, pos + else + while true do + key, pos = parse_str_val(str, pos, true); + if key == nil then error("Expected a string key") end + + pos = skip_delim(str, pos, ":", true); -- true -> error if missing. + obj[key], pos = parse_impl(str, pos); + + pos, delim_found = skip_delim(str, pos, "}"); + if delim_found then return obj, pos end + + pos, delim_found = skip_delim(str, pos, ","); + if not delim_found then error("Expected semicolon or comma") end + end + end + elseif c == "[" then + pos = pos + 1 + + local arr = array {}; + local val; + local delim_found = true; + + while true do + val, pos = parse_impl(str, pos, "]"); + if val == json_end then return arr, pos end + if not delim_found then error("Comma missing between array items: " .. str:sub(pos, pos + 25)) end + + arr:push(val); + pos, delim_found = skip_delim(str, pos, ","); + end + elseif c == "\"" then -- Parse a string. + return parse_str_val(str, pos, false); + elseif c == "-" or c:find("%d") then -- Parse a number. + return parse_num_val(str, pos); + elseif c == end_delim then -- End of an object or array. + return json_end, pos + 1, true; + elseif str:sub(pos, pos + 3) == "null" then + return nil, pos + 4; + elseif str:sub(pos, pos + 3) == "true" then + return true, pos + 4; + elseif str:sub(pos, pos + 4) == "false" then + return true, pos + 5; + else + error(table.concat { "Invalid json syntax starting at position ", pos, ": ", str:sub(pos, pos + 10) }); + end +end + +local function stringify_impl(obj, all, indent_str, n) + local s = {}; -- We'll build the string as an array of strings to be concatenated. + local kind = kind_of(obj); -- This is 'array' if it's an array or type(obj) otherwise. + + if kind == "array" then + for i, val in ipairs(obj) do + s[i] = stringify_impl(val, all, indent_str, n + 1); + end + + if not indent_str then + return "[" .. table.concat(s, ",") .. "]"; + elseif #s == 0 then + return "[]"; + else + local indent = "\n" .. string.rep(indent_str, n + 1); + return table.concat { + "[", + indent, table.concat(s, "," .. indent), + "\n", string.rep(indent_str, n), "]" + }; + end + elseif kind == "table" then + for k, v in pairs(obj) do + local sep = indent_str and ": " or ":"; + local val = stringify_impl(v, all, indent_str, n + 1); + if val ~= nil then + if type(k) == "string" then + s[#s + 1] = stringify_impl(k) .. sep .. val; + elseif type(k) == "number" then + s[#s + 1] = "\"" .. k .. "\"" .. sep .. val; + elseif type(k) == "boolean" then + s[#s + 1] = "\"" .. k .. "\"" .. sep .. val; + end + end + end + + if not indent_str then + return "{" .. table.concat(s, ",") .. "}"; + elseif #s == 0 then + return "{}"; + else + local indent = "\n" .. string.rep(indent_str, n + 1); + return table.concat { + "{", + indent, table.concat(s, "," .. indent), + "\n", string.rep(indent_str, n), "}" + }; + end + return "{" .. table.concat(s, ",") .. "}"; + elseif kind == "string" then + return "\"" .. escape_str(obj) .. "\""; + elseif kind == "number" then + return tostring(obj); + elseif kind == 'boolean' then + return tostring(obj); + elseif kind == "nil" then + return "null"; + elseif all then + return tostring(obj); + else + return nil; + end +end + +function exports.stringify(obj, indent_str) + if indent_str == true then + indent_str = " "; + end + return stringify_impl(obj, false, indent_str, 0); +end +function exports.pretty(obj) + return stringify_impl(obj, true, " ", 0); +end +exports.null = {}; -- This is a one-off table to represent the null value. + +---@param str string +---@return unknown +function exports.parse(str) + local obj = parse_impl(str, 1); + return obj; +end + +return exports; diff --git a/mod/path.lua b/mod/path.lua new file mode 100644 index 0000000..70df128 --- /dev/null +++ b/mod/path.lua @@ -0,0 +1,160 @@ +---@diagnostic disable: cast-local-type +local exports = {}; + +---@param ... string +---@return array parts +---@return integer? i +---@return boolean dir +function exports.split(...) + --- @type integer | nil + local i = 0; + --- @type string[] + local res = {}; + local dir = false; + + local _, start = (... or ""):find("^/+"); + if start == nil then + start = 1; + else + i = nil; + dir = true; + end + + for i = 1, select("#", ...) do + local p = select(i, ...); + + if p ~= nil then + for part, slashes in p:gmatch("([^/]+)(/*)", start) do + dir = #slashes > 0; + if part == ".." then + if #res == 0 then + if i ~= nil then + i = i + 1; + end + else + res[#res] = nil; + end + elseif part ~= "." and part ~= "" then + res[#res + 1] = part; + end + end + end + end + + return res, i, dir; +end + +---@param parts array +---@param i integer? +---@param dir boolean | "all" +function exports.stringify(parts, i, dir) + local a = table.concat(parts, "/"); + + if #parts == 0 then + if i == nil then + return dir and "/" or "/."; + elseif i == 0 then + return dir and "./" or ""; + else + local res = (".."):rep(i, "/"); + return dir and res .. "/" or res; + end + end + + local res = i and ("../"):rep(i) or "/"; + + if dir then + return res .. a .. "/"; + else + return res .. a; + end +end + +---@param ... string +---@return string path +---@return boolean dir +function exports.join(...) + local parts, i, dir = exports.split(...); + return exports.stringify(parts, i, dir), dir; +end + +---@param ... string +---@return string +function exports.join_dir(...) + local p, i = exports.split(...); + return exports.stringify(p, i, true); +end +---@param ... string +---@return string +function exports.join_file(...) + local p, i = exports.split(...); + return exports.stringify(p, i, false); +end +---@param p string +---@return boolean +function exports.is_dir(p) + return p:match "/$" ~= nil; +end + +---@param ... string +---@return string +---@return boolean dir +function exports.chroot(...) + local parts, i, dir = exports.split((...)); + + for i = 2, select("#", ...) do + local new_parts, _, new_dir = exports.split((select(i, ...))); + dir = new_dir; + + for j = 1, #new_parts do + parts[#parts + 1] = new_parts[j]; + end + end + + return exports.stringify(parts, i, dir), dir; +end + +---@param ... string +---@return string +function exports.cwd(...) + local parts, i, dir = exports.split((...)); + + for i = 2, select("#", ...) do + local new_parts, new_i, new_dir = exports.split((select(i, ...))); + dir = new_dir; + + if new_i == nil then + parts = new_parts; + i = nil; + else + for _ = 1, new_i do + parts[#parts] = nil; + end + + if new_parts ~= nil then + local offset = parts and #parts or 0; + for i = 1, #new_parts do + parts[i + offset] = new_parts[i]; + end + end + end + end + + return exports.stringify(parts, i, dir); +end + +---@param ... string +---@return string +function exports.dirname(...) + local parts, i = exports.split(...); + parts[#parts] = nil; + return exports.stringify(parts, i, true); +end + +---@param ... string +---@return string +function exports.filename(...) + return table.remove(exports.split(...)) or ""; +end + +return exports; diff --git a/mod/pipes.lua b/mod/pipes.lua new file mode 100644 index 0000000..95e4896 --- /dev/null +++ b/mod/pipes.lua @@ -0,0 +1,145 @@ +local io = io or require "io"; +local exports = {}; + +--- The default buffer size used by read operations +exports.BUFF_SIZE = 4096; + +--- @alias Reader fun(n: number, no_throw?: boolean): string?, string? +--- @alias Writer fun(data: string | nil, no_throw?: boolean): true?, string? + +--- Creates a reader from the path +--- @param path string +--- @return Reader? result +function exports.reader(path, force_file) + if path == "-" and not force_file then return exports.stdin end + return exports.reader_from(io.open(path, "rb")) +end +--- @param f? file* +--- @return Reader? result +function exports.reader_from(f, ...) + f = assert(f, ...); + return function (n) + if n == nil then n = exports.BUFF_SIZE end + + if n < 0 then + if f ~= nil then + f:close(); + f = nil; + end + elseif f == nil then + error("File closed", 2); + else + local res = f:read(n); + + if res == nil then + f:close(); + f = nil; + return ""; + else + return res; + end + end + end +end + +--- Creates a writer from the path +--- @param path string +--- @return Writer? result +function exports.writer(path, force_file) + if path == "-" and not force_file then return exports.stdout end + return exports.writer_from(io.open(path, "wb")) +end +--- @param f? file* +--- @return Writer? result +function exports.writer_from(f, ...) + f = assert(f, ...); + return function (data) + if data == nil then + if f ~= nil then + f:close(); + f = nil; + end + + return true, nil; + elseif f == nil then + error("File closed", 2); + else + local res, w_err = f:write(data); + + if res == nil then + error(w_err, 2); + else + return true; + end + end + end +end + +--- Writes the full contents of src to dst. Since src is read until its end, it will be closed after this function +--- @param src Reader +--- @param dst Writer +--- @param close_dst boolean? Wether or not to close dst after copying +function exports.copy(src, dst, close_dst) + local res, buff, err, failed; + + while true do + buff, err = src(4096, true); + + if buff == nil and err ~= nil then + error(err, 2); + elseif buff == nil or buff == "" then + break; + else + res, err = dst(buff, true) + if res == nil then + error(err, 2); + end + end + + end + + if close_dst then + res, err = dst(nil, true); + + if res == nil then + error(err, 2); + end + end + + if failed then + error(err, 2); + end +end + +function exports.loader(...) + local arr = {...}; + local i = 1; + + return function () + while true do + local el = arr[i]; + + if el == "" then + i = i + 1; + elseif el == nil then + return nil; + elseif type(el) == "function" then + local buff = el(); + if buff ~= "" then + return buff; + else + i = i + 1; + end + elseif type(el) == "string" then + i = i + 1; + return el; + else + error("Loader may consist only of strings and functions"); + end + end + end +end + +exports.stdin = exports.writer_from(io.stdin); +exports.stdout = exports.writer_from(io.stdout); +exports.stderr = exports.writer_from(io.stderr); diff --git a/mod/promise.d.lua b/mod/promise.d.lua new file mode 100644 index 0000000..11f930c --- /dev/null +++ b/mod/promise.d.lua @@ -0,0 +1,50 @@ +--- @meta promise + +--- @alias promise promiselib | { when: fun(ful: fun(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6, p7: T7, p8: T8, p9: T9, p10: T10), rej: fun(err)) } + +--- @class promiselib +promise = {}; +promise.__index = promise; + +--- @generic T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 +--- @param on_ful? fun(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6, p7: T7, p8: T8, p9: T9, p10: T10) +--- @param on_rej? fun(err: any) +function promise:when(on_ful, on_rej) end + +--- @generic T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 +--- @param self promise +--- @returns T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 +function promise:await() end + +--- @generic T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 +--- @param func fun(ful: fun(v: T1, v: T2, v: T3, v: T4, v: T5, v: T6, v: T7, v: T8, v: T9, v: T10), rej: fun(val: any)) +--- @return promise +function promise.mk(func) end + +--- @generic T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 +--- @param p1? T1 +--- @param p2? T2 +--- @param p3? T3 +--- @param p4? T4 +--- @param p5? T5 +--- @param p6? T6 +--- @param p7? T7 +--- @param p8? T8 +--- @param p9? T9 +--- @param p10? T10 +--- @return promise +function promise.resolve(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) end + +--- @param err any +--- @return promise +function promise.reject(err) + return promise.mk(function (_, rej) + if err and type(err.when) == "function" then + err:when(rej, rej); + else + rej(err); + end + end); +end + +return promise; diff --git a/mod/promise.lua b/mod/promise.lua new file mode 100644 index 0000000..499b66a --- /dev/null +++ b/mod/promise.lua @@ -0,0 +1,134 @@ +local loop = require "evn-loop"; +local promise = {}; +promise.__index = promise; + +function promise:when(on_ful, on_rej) end + +function promise:await() + local curr = coro.running(); + local res = nil; + + self:when(function (...) + if coro.running() == curr then + res = box(true, ...); + else + coro.transfer(curr, true, ...); + end + end, function (...) + if coro.running() == curr then + res = box(false, ...); + else + coro.transfer(curr, false, ...); + end + end); + + local function process(ok, ...) + if ok then + return ...; + else + error((...), 2); + end + end + + if res then + return process(unbox(res)); + else + return process(loop.interrupt()); + end +end + +function promise.mk(func) + local state = 0; + local val = nil; + + --- @type array + local ful_handles = array {}; + --- @type array + local rej_handles = array {}; + + local function invoke_handle(fun, arg_box) + loop.push(function () + pcall(fun, unbox(arg_box)); + end); + end + + local function fulfill(...) + if state == 0 then + state = 1; + val = box(...); + + for i = 1, #ful_handles do + invoke_handle(ful_handles[i], val); + end + end + end + + local function reject(err) + if state == 0 then + state = 2; + val = box(err); + + for i = 1, #ful_handles do + invoke_handle(rej_handles[i], val); + end + end + end + + local ok, err = pcall(func, fulfill, reject); + if not ok then reject(err) end + + return setmetatable({ + when = function (_, on_ful, on_rej) + if state == 0 then + ful_handles:push(on_ful); + rej_handles:push(on_rej); + elseif state == 1 then + return invoke_handle(on_ful, val); + elseif state == 2 then + return invoke_handle(on_rej, val); + end + end + }, promise); +end + +function promise.resolve(...) + local res = box(...); + + return promise.mk(function (ful, rej) + local passed = 0; + + for i = 1, res.n do + if type(res[i]) == "table" and type(res[i].when) == "function" then + res[i]:when( + function (...) + res[i] = ...; + passed = passed + 1; + + if passed >= res.n then + ful(unbox(res)); + end + end, + function (err) rej(err) end + ); + else + passed = passed + 1; + end + end + + if passed == res.n then + ful(unbox(res)); + end + end); +end + +function promise.reject(err) + return promise.mk(function (_, rej) + if err and type(err.when) == "function" then + err:when(rej, rej); + else + rej(err); + end + end); +end + +return promise; diff --git a/mod/tal/cli.lua b/mod/tal/cli.lua new file mode 100644 index 0000000..aa42779 --- /dev/null +++ b/mod/tal/cli.lua @@ -0,0 +1,222 @@ +local arg_parse = require "args"; +local path = require "path"; +local traceback = require "traceback"; +local root = require "root"; + +local exports = {}; + +function exports.stacktrace_call(func, ...) + return xpcall(func, function (err) + local trace = traceback(2, "\t"); + + io.stderr:write "Unhandled error: "; + + if type(err) == "string" then + print(err); + else + pprint(err); + end + + print(trace); + + return err; + end, ...); +end + +function exports.load_eval(src, name, env) + local f, err = load(iterate { "return ", src }, name, "t", env); + if f == nil then + f, err = load(src, name, "t", env); + end + + return f, err; +end + +function exports.repl() + local mod = root.mk(path.join(root.name, "../")); + function mod.export() + error "Can't export in the REPL"; + end + + mod.exports = nil; + mod.returns = {}; + + mod.env.export = mod.export; + mod.env.import = mod.import; + mod.env.require = mod.require; + mod.env.module = mod; + local local_err_shown = false; + + while true do + local cont = true; + + exports.stacktrace_call(function () + local src = ""; + local err; + + repeat + local done = false; + local f; + + if src == "" then + io.stderr:write "> "; + else + io.stderr:write "... "; + end + + --- @type string + local line = io.stdin:read("l"); + if line == nil then + cont = false; + return; + elseif line == "" then + break + end + + src = src .. "\n" .. line; + f, err = exports.load_eval(src, "", mod.env); + + if f ~= nil then + if not local_err_shown then + local i = 1; + local locals = array {}; + + while true do + local n = debug.getlocal(f, i); + if not n then break end + + if not n:match("^%(") then + locals:push(n); + end + + i = i + 1; + end + + if #locals > 0 then + local_err_shown = true; + print "You have defined the following locals in your code:"; + print(locals:join ", "); + print "Consider declaring them as globals instead (\"a = 10\" and \"function a() ... end\")"; + end + end + + pprint(f()); + + done = true; + end + until done; + + if err ~= nil then + error(err, 0); + end + + return true; + end); + + if not cont then return end + + promise.resolve():await(); + end +end + +function exports.exec(mod, requires, compile, ...) + exports.stacktrace_call(function (...) + for i = 1, #requires do + mod.require(requires[i]) + end + + local func = compile(); + + mod.main = true; + mod.returns = box(func(...)); + + if mod.returns.n > 0 then + mod.exports = mod.returns[1]; + end + + if type(mod.exports) == "function" then + return mod.exports(...); + elseif type(mod.exports) == "table" and type(mod.exports.main) == "function" then + return mod.exports.main(...); + end + end, ...); +end + +function exports.main(...) + require "core"; + + local file; + local args = array {}; + local requires = array {}; + local eval; + + arg_parse({ + function (v) + if eval then + eval:push(v); + elseif file ~= nil then + args:push(v); + else + file = v; + end + + return true; + end, + require = function (v) + requires:push(v); + return true; + end, + eval = function () + eval = array {}; + return false, true; + end, + version = function () + print(str("TAL v", TAL)); + print("Created /w love and dread by TopchetoEU"); + exit(); + end, + help = function () + print(str("TAL v", TAL, " by TopchetoEU")); + print "A pseudo-runtime on top of my beloved Lua. Includes a better module system and"; + print "an improved set of stdlibs to make your life easier."; + print "PLEASE DON'T USE IN PRODUCTION, MIGHT BREAK AT ANY SECOND"; + print "Options:"; + print "\t--require (-r) [name]: Requires the given package before execution, similar to \"lua -l\""; + print "\t--help (-h): Shows this message"; + print "\t--version: Shows the version"; + print "\t--eval (-e): Evaluates the rest of the arguments"; + print "\t--: Passes the rest of the arguments as arguments"; + + exit(); + end, + + r = "require", + h = "help", + e = "eval", + }, ...); + + if eval then + local mod = root.mk(path.join(root.name, "../")); + return exports.exec(mod, requires, function () + return assert(exports.load_eval(eval:join " ", "", mod.env)); + end, unpack(args)); + elseif file then + return exports.stacktrace_call(function (...) + if not file:match "^/" then + file = "./" .. file; + end + + local mod = root.import(file); + + if type(mod) == "function" then + return mod(...); + elseif mod and type(mod.main) == "function" then + return mod.main(...); + end + end, ...); + else + return exports.repl(); + end +end + +return exports; diff --git a/mod/traceback.lua b/mod/traceback.lua new file mode 100644 index 0000000..881f733 --- /dev/null +++ b/mod/traceback.lua @@ -0,0 +1,34 @@ +return function(i, prefix) + i = i + 1; + local lines = array {}; + + while true do + local info = debug.getinfo(i, "Snl"); + if info == nil then break end + + local name = info.name; + local location = info.short_src; + + if location == "[C]" then + location = "at "; + elseif string.find(location, "[string", 1, true) then + location = "at "; + else + location = "at " .. location; + end + + if info.currentline > 0 then + location = location .. ":" .. info.currentline; + end + + if name ~= nil then + lines:push(prefix .. location .. " in " .. name); + else + lines:push(prefix .. location); + end + + i = i + 1 + end + + return lines:join "\n"; +end