add preliminary half-tested half-working implementation of the `io` global

main
cinder 6 months ago
parent 9813dee130
commit a1e6b75b22

@ -161,13 +161,20 @@ local function exportFile(srcpath, dstpath)
end end
local function buildKernel(dst) local function buildKernel(dst)
local sources = fs.list('src/kernel/') local srcdir = 'src/kernel/'
local sources = fs.list(srcdir)
table.sort(sources) table.sort(sources)
for i, path in ipairs(sources) do
path = srcdir .. path
sources[i] = path
assert(loadfile(path)) --validate syntax
end
function sources:lines() function sources:lines()
return coroutine.wrap(function() return coroutine.wrap(function()
for i, path in ipairs(sources) do for i, path in ipairs(sources) do
local f = assert(io.open('src/kernel/' .. path)) local f = assert(io.open(path))
for l in f:lines() do for l in f:lines() do
coroutine.yield(l) coroutine.yield(l)
end end

@ -3,7 +3,6 @@ local bootfs = ...
-- ...but [lua bios, openloader] don't, so fall back to the usual way -- ...but [lua bios, openloader] don't, so fall back to the usual way
bootfs = bootfs or computer.getBootAddress() bootfs = bootfs or computer.getBootAddress()
--store all functions we need in locals so userspace mischief can't replace them --store all functions we need in locals so userspace mischief can't replace them
--prefix 'k' (for kernel) on any functions userspace will also have a version of to ensure kernelspace --prefix 'k' (for kernel) on any functions userspace will also have a version of to ensure kernelspace
--function usage is intentional (ie prevent accidentally using the kernel's `load` in a userspace `loadfile`) --function usage is intentional (ie prevent accidentally using the kernel's `load` in a userspace `loadfile`)
@ -12,9 +11,9 @@ local assert, checkArg, error, kload, ipairs, next, pairs, rawequal, rawset, set
local invoke = component.invoke local invoke = component.invoke
local kpullSignal, shutdown, uptime = computer.pullSignal, computer.shutdown, computer.uptime local kpullSignal, shutdown, uptime = computer.pullSignal, computer.shutdown, computer.uptime
local co_create, co_status, kresume, kyield = coroutine.create, coroutine.status, coroutine.resume, coroutine.yield local co_create, co_status, kresume, kyield = coroutine.create, coroutine.status, coroutine.resume, coroutine.yield
local huge = math.huge local huge, math_type = math.huge, math.type
local format = string.format local find, format, match, sub = string.find, string.format, string.match, string.sub
local pack, unpack = table.pack, table.unpack local concat, pack, unpack = table.concat, table.pack, table.unpack
local big = math.maxinteger or huge --some large amount to read at once in kreadfile local big = math.maxinteger or huge --some large amount to read at once in kreadfile
@ -26,4 +25,44 @@ _ENV = setmetatable({}, {
__newindex = function(self, key) return error(format('assigned global %q', key), 2) end, __newindex = function(self, key) return error(format('assigned global %q', key), 2) end,
}) })
local isFile --function(obj:any):boolean, boolean -- returns (is obj a file?), (is obj an open file?)
--[[function(index:number, val:any, types:string...):string -- asserts `types` contains the type of `val`. similar to `checkArg`.
recognizes all possible returns from `type()` as well as ['integer', 'float', 'file']. unlike io.type(), 'file' ignores whether the file is open or closed.
on success, returns the element of `types` that matched.
on failure, throws an error originating from the caller's caller that mimicks built-in type errors. `index` should be the 1-based index of the argument that was the wrong type.
]]
local checkArgEx
do
-- tail-recursive varargs part of checkArgEx
local function check(val, t, want, ...)
if t == want then
return want
elseif want == 'integer' or want == 'float' then
if math_type(val) == want then
return want
end
elseif want == 'file' then
if isFile(val) then
return want
end
elseif not want then
return nil
else
return check(val, t, ...)
end
end
function checkArgEx(i, val, ...)
local t = type(val)
local ok = check(val, t, ...)
if ok then
return ok
else
error(format('bad argument #%d (%s expected, got %s)', i, concat({...}, ' or '), t), 3)
end
end
end
-- vi: set ts=2: -- vi: set ts=2:

@ -0,0 +1,307 @@
local openNull, openPath
do
local filedata = setmetatable({}, { __mode = 'k' }) --weak-keyed mapping of file object to data/inner (fh of wrapFile)
local filemt = { __metatable = '<file>' }
filemt.__index = filemt
function isFile(obj)
if filedata[obj] then
-- if this is true, it's an object we control, so it's safe to trust attribute access
return true, not obj._closed
else
return false
end
end
local function wrapFile(mode, fh)
local rwa, bin, plus, b2 = match(mode or 'r', '^([rwa])(b?)(%+?)(b?)$') --match all of x, xb, x+, xb+, x+b
if not rwa or (bin ~= '' and b2 ~= '') then return nil, 'bad mode' end
bin = bin .. b2 --when (bin == '') xor (b2 == ''), this is logical or
local read = rwa == 'r' or plus
local write = rwa ~= 'r' or plus
local append = rwa == 'a'
local update = plus ~= ''
local file = setmetatable({}, filemt)
filedata[file] = fh
if read then
fh._file_rbuf = ''
end
if write then
fh._file_wbuf, fh._file_wmode, fh._file_wcap = '', 'full', nil --nil capacity means default, which may vary with e.g. system memory usage
end
return file
end
local function null_close() return true end
local function null_read() return nil end --always eof
local function null_write() return true end --eats data
--null devices can't be seeked
function openNull(mode)
return assert(wrapFile(mode, { close = null_close, read = null_read, write = null_write }))
end
local function path_close(fh) return invoke(fh.address, 'close', fh.handle) end
local function path_read(fh, n) return invoke(fh.address, 'read', fh.handle, n) end
local function path_write(fh, data) return invoke(fh.address, 'write', fh.handle, data) end
local function path_seek(fh, ...) return invoke(fh.address, 'seek', fh.handle, ...) end
function openPath(mode, addr, path)
local fd, err = invoke(addr, 'open', path, mode) --should we validate `mode` first?
if not fd then
return nil, err
end
return assert(wrapFile(mode, { address = addr, handle = fd, close = path_close, read = path_read, write = path_write, seek = path_seek }))
end
--file method helpers
local function assertOpen(file, how)
local fh = filedata[file]
if fh._file_closed then error('attempt to use a closed file', 3) end
if how == 'read' then
if not fh._file_rbuf then return nil, 'file not readable' end
elseif how == 'write' then
if not fh._file_wbuf then return nil, 'file not writable' end
elseif how == 'seek' then
if not fh.seek then return nil, 'file not seekable' end
elseif how then
error('bad how: should be read, write, seek, or nil', 2)
end
return fh
end
local function flush(fh)
local ok, err = fh:write(fh._file_wbuf)
if ok then
fh._file_wbuf = ''
return true
else
return nil, err
end
end
local function read1(fh, n)
if type(n) == 'number' then
while #fh._file_rbuf < n do
local blocksz = n - #fh._file_rbuf
if blocksz < 512 then blocksz = 512 end --TODO tune default capacity to e.g. free memory
local data, err = fh:read(blocksz)
if err then
return nil, err
elseif not data then --eof
break
end
fh._file_rbuf = fh._file_rbuf .. data
end
local data = sub(fh._file_rbuf, 1, n)
fh._file_rbuf = sub(fh._file_rbuf, n + 1)
return data
elseif n == 'a' then
local parts = { fh._file_rbuf }
local chunk, err
repeat
chunk, err = fh:read(512)
parts[#parts+1] = chunk
until not chunk
local cat = concat(parts)
if err then
fh._file_rbuf = cat
return nil, err
else
fh._file_rbuf = ''
return cat
end
else
error('TODO read1 patterns')
end
end
local function write1(fh, data)
local wbuf = fh._file_wbuf .. data
fh._file_wbuf = wbuf
local mode = fh._file_wmode
if mode == 'no' then
if wbuf ~= '' then return flush(fh) end
elseif mode == 'full' then
local cap = fh._file_wcap or 512 --TODO tune default capacity to e.g. free memory
if #fh._file_wbuf >= cap then
return flush(fh)
end
else --for line buffering, only flush complete lines
local i1, i2 = find(fh._file_wbuf, '.+[\r\n]') --`i2` is the last cr or lf in the buffer
if i1 then --if there are any newlines
local rest = sub(fh._file_wbuf, i2 + 1)
fh._file_wbuf = sub(fh._file_wbuf, 1, i2)
local ok, err = flush(fh)
if ok then fh._file_wbuf = rest end
return ok, err
else --no newlines, nothing to flush
return true
end
end
end
--file methods
function filemt:close()
local fh = assertOpen(self)
if fh._file_wbuf then
local ok, err = flush(fh)
if not ok then return nil, err end --XXX should close ever fail? what do we expect the caller to do?
end
fh:close()
fh._file_closed = true
end
filemt.__close = filemt.close --for future lua 5.4 compat
function filemt:flush()
local fh, err = assertOpen(self, 'write')
if not fh then return nil, err end
local ok, err = flush(fh)
if not ok then return nil, err end
return self
end
function filemt:lines(...)
local pats = pack(...)
for i = 1, pats.n do
local pat = pats[i]
local t = checkArgEx(i, pat, 'string', 'integer')
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
error(format('bad argument #%d to \'lines\' (invalid format)', i), 2)
end
end
if pats.n == 0 then
pats[1], pats.n = 'l', 1
end
local ok, err = assertOpen(self, 'read')
if not ok then error(err, 2) end --more informative than the error from `for line in nil, err do end`
return function()
local ret = {}
for i = 1, pats.n do
local ok, err = read1(fh, pats[i])
if not ok then
if err then error(err, 2) end
break
end
ret[i] = ok
end
return unpack(ret, 1, pats.n)
end
end
function filemt:read(...)
local args = pack(...)
for i = 1, args.n do
local pat = args[i]
local t = checkArgEx(i, pat, 'string', 'integer')
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
error(format('bad argument #%d to \'read\' (invalid format)', i), 2)
end
end
if args.n == 0 then
args[1], args.n = 'l', 1
end
local fh, err = assertOpen(self, 'read')
if not fh then return nil, err end
local args = pack(...)
local iend = args.n
for i = 1, args.n do
local ok, err = read1(fh, args[i])
if ok then
args[i] = ok
else
iend = i + 1
args[i] = nil
args[i + 1] = err
break
end
end
return unpack(args, 1, iend)
end
function filemt:seek(whence, offset)
if type(whence) == 'number' and rawequal(offset, nil) then
checkArgEx(1, whence, 'integer') --produce a better error message citing arg 1
whence, offset = 'cur', whence
end
checkArgEx(1, whence, 'string', 'nil')
checkArgEx(2, offset, 'integer', 'nil')
local fh, err = assertOpen(self, 'seek')
if not fh then return nil, err end
--before seeking, flush any buffered writes
if fh._file_wbuf then
local ok, err = flush(fh)
if not ok then return nil, err end
end
local ok, err = fh:seek(whence or 'cur', offset or 0)
if not ok then return nil, err end
local tell = (whence == 'cur' or not whence) and (offset == 0 or not offset) --true if we didn't move ie ftell()
--after seeking, if we moved, invalidate the read buffer
if (whence and whence ~= cur) or (offset and offset ~= 0) then
fh._file_rbuf = fh._file_rbuf and ''
end
return ok
end
function filemt:setvbuf(mode, size)
checkArg(1, mode, 'string')
if mode ~= 'no' and mode ~= 'line' and mode ~= 'full' then
error(format('bad argument #1 to \'setvbuf\' (invalid option %q)', mode), 2)
end
checkArgEx(2, size, 'integer', 'nil')
local fh, err = assertOpen(self)
if not fh then return nil, err end
if not fh._file_wbuf then
return true --on readonly files, setvbuf is a noop
end
fh._file_wmode, fh._file_wcap = mode, size
--don't flush now; we don't have a way to propagate errors. the next write (or close) will take care of it.
return true
end
function filemt:write(...)
local args = pack(...)
for i = 1, args.n do
local arg = args[i]
local t = checkArgEx(i, arg, 'string', 'number')
if t ~= 'string' then args[i] = tostring(arg) end
end
local fh, err = assertOpen(self, 'write')
if not fh then return nil, err end
for i = 1, args.n do
local ok, err = write1(fh, args[i])
if not ok then return nil, err end
end
return self
end
end
-- vi: set ts=2:

@ -9,7 +9,7 @@ given <https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>:
]] ]]
envBase._OSVERSION = 'kitn 0.1.2' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver" envBase._OSVERSION = 'kitn 0.2.0-beta.1' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver"
envBase.kitn = {} envBase.kitn = {}
envBase._G, envBase.load = nil envBase._G, envBase.load = nil
@ -84,6 +84,50 @@ function fillEnv(proc, uenv)
uenv.load, uenv.loadfile, uenv.dofile = uload, uloadfile, udofile uenv.load, uenv.loadfile, uenv.dofile = uload, uloadfile, udofile
local stdin, stdout = openNull('r'), openNull('w')
local uio; uio = {
stdin = stdin, stdout = stdout, stderr = stdout,
close = function(file) return (file or uio.stdout):close() end,
flush = function() return uio.stdout:flush() end,
input = function(file)
local t = checkArgEx(1, file, 'string', 'table', 'nil')
if t ~= 'nil' then
uio.stdin = (t == 'string' and assert(uio.open(file, 'r')) or file)
end
return uio.stdin
end,
lines = function(filename, ...)
checkArg(1, filename, 'string', 'nil')
error('TODO io.lines') --iff we open a file, we need to close it
end,
open = function(filename, mode) return openPath(mode, bootfs, filename) end,
output = function(file)
local t = checkArgEx(1, file, 'string', 'table', 'nil')
if t ~= nil then
uio.stdout = (t == 'string' and assert(uio.open(file, 'w')) or file)
end
return uio.stdout
end,
--popen is unimplemented because we don't have a notion of a shell
read = function(...) return uio.stdin:read(...) end,
tmpfile = function()
--use math.random() to pick a filename on computer.tmpAddress() that doesn't exist yet and create it
error('TODO io.tmpfile') --file is deleted on process exit, requires atExit mechanism
end,
type = function(obj)
local file, opened = isFile(obj)
if opened then
return 'file'
elseif file then
return 'closed file'
else
return nil
end
end,
write = function(...) return uio.stdout:write(...) end,
}
uenv.io = uio
--env will automatically mirror envBase.kitn here --env will automatically mirror envBase.kitn here
local ukitn = uenv.kitn local ukitn = uenv.kitn
Loading…
Cancel
Save