neyuki 0.6.3

A small Lua-like scripting language implemented in Rust
local table = require("@neyuki/table")

local io = {}

function checkOptionalString(s: string?, name: string)
    if (s != nil and type(s) != "string") then error(name .. " must be a string") end
end

function checkFormat(format: string | number | nil): string | number
    format = format ?? "l"
    if (type(format) == "number") then
        if (format < 0) then error("format must be a non-negative number of characters") end
        return format
    end
    if (type(format) != "string") then error("format must be a string or a number") end
    if (format == "l" or format == "L" or format == "a" or format == "n") then return format end
    error("invalid read format `" .. format .. "` (expected l, L, a, n or a number)")
end

-- Reads one value from stdin according to `format` (already validated).
function readOne(format: string | number): string | number | nil
    if (type(format) == "number") then return __io_read_chars(format) end
    if (format == "l") then return __io_read_line(false) end
    if (format == "L") then return __io_read_line(true) end
    if (format == "a") then return __io_read_all() end
    return __io_read_number()
end

function joinValues(values: {any}): string
    local parts = {}
    for index, value in values do
        if (type(value) != "string" and type(value) != "number") then
            error("argument " .. tostring(index) .. " must be a string or a number, got " .. type(value))
        end
        parts[index] = tostring(value)
    end
    return table.concat(parts)
end

function makeReader(): () -> ...any
    return function(...: string | number): ...any
        local formats = {...}
        if (#formats == 0) then return readOne("l") end
        local results = {}
        for index, format in formats do
            local value = readOne(checkFormat(format))
            results[index] = value
            -- Once input runs out, the remaining formats cannot be satisfied.
            if (value == nil) then break end
        end
        return table.unpack(results)
    end
end

function makeLines(): (string | number | nil) -> () -> string | number | nil
    return function(format: string | number | nil): () -> string | number | nil
        local resolved = checkFormat(format)
        return function(): string | number | nil
            return readOne(resolved)
        end
    end
end

function makeWriter(stream: string): (...any) -> Stream
    return function(...: any): Stream
        __io_write(stream, joinValues({...}))
        return io[stream]
    end
end

function makeStream(name: string): Stream
    local stream = {}
    stream.name = name

    function stream.isTerminal(self: Stream): boolean
        return __io_isatty(name)
    end

    if (name == "stdin") then
        local read = makeReader()
        local lines = makeLines()
        function stream.read(self: Stream, ...: string | number): ...any
            return read(...)
        end
        function stream.lines(self: Stream, format: string | number | nil): () -> string | number | nil
            return lines(format)
        end
    else
        local write = makeWriter(name)
        function stream.write(self: Stream, ...: any): Stream
            return write(...)
        end
        function stream.flush(self: Stream): ()
            __io_flush(name)
        end
    end

    return table.freeze(stream)
end

io.stdin = makeStream("stdin")
io.stdout = makeStream("stdout")
io.stderr = makeStream("stderr")

-- Reads from stdin. Each format is one of:
--   "l"      a line without its trailing newline (the default)
--   "L"      a line including its trailing newline
--   "a"      everything remaining on stdin
--   "n"      a whitespace-delimited number
--   number   that many characters
-- Returns nil for a format once input is exhausted.
io.read = makeReader()

-- Returns an iterator that reads stdin with `format` until input runs out.
io.lines = makeLines()

-- Writes every argument to stdout without separators or a trailing newline.
io.write = makeWriter("stdout")

function io.flush(): ()
    __io_flush("stdout")
end

-- Prints `text` (if any), flushes stdout and reads one value from stdin.
function io.prompt(text: string?, format: string | number | nil): string | number | nil
    checkOptionalString(text, "text")
    if (text != nil) then __io_write("stdout", text) end
    __io_flush("stdout")
    return readOne(checkFormat(format))
end

return table.freeze(io)