Host capabilities
there is no print and no open. the outside world is a set of peers, and a
peer is an address:
(say (call '(host stdout) '(line "hello")))
hello
'ok
('host name) has the same shape as a task’s address on purpose. a call site
cannot tell a peer written in rust from one written here, so anything doable to
a task is doable to the world: call it, send to it, monitor it, hand its
address to somebody else.
The set
('host stdout) ('write v) ('line v)
('host stdin) ('read) ('read prompt)
('host files) ('read path) ('root)
('host disk) ('write path text)
('host clock) ('now) ('sleep ms) ('after ms to msg)
('host gate) ('dial spec) ('listen spec)
('host modules) ('fetch url ref)
each is a tokio task holding one end of a channel, and naj decides which to
register. a peer is the unit of granting. a world that registers no clock does
not have a slower clock, it has none, and a program built out of restart counts
rather than restart rates runs in that world.
('host modules) is the one thing in the tree that reaches the network, and
only naj --lock registers it:
(say (attempt (lambda () (call '(host modules) '(fetch "https://x/y" "HEAD")))))
('throw 'callee-down ('host 'modules) 'throw . 'noproc)
the same callee-down a dead task gives. a capability that was not granted
fails where it was used, not at startup.
Clock
(define t0 (call '(host clock) '(now)))
(call '(host clock) '(sleep 30))
(say (> (- (call '(host clock) '(now)) t0) 20))
(send '(host clock) (list 'after 10 self 'ding))
(say (receive))
1
'ding
after is a message posted later to somebody else, which is why the timeout in
Calls and replies needs nothing in the loop: it arms an after
carrying the same reply id the real answer would. each timer is a task that
sleeps and then sends. nothing is polled.
Reading and writing
files reads and disk writes. two peers rather than two verbs on one,
because a peer is what gets granted and read-only is worth granting.
(say (call '(host disk) '(write "/tmp/naj-out.txt" "written by narju\n")))
(say (call '(host files) '(read "/tmp/naj-out.txt")))
(say (call '(host files) '(root)))
'ok
written by narju
/home/thorn/.cache/narju/modules
root is the module store, the only path need joins against. see
Modules.
failures come back as ('io . text):
(say (attempt (lambda () (call '(host files) '(read "/tmp/definitely-not-here")))))
('throw 'io . "/tmp/definitely-not-here: No such file or directory (os error 2)")
Two adapters wear the stdout name
down a pipe, stdout and stdin are separate adapters. at a terminal, one
console is registered under both names, so the channel’s order is the
screen’s order. two channels would let a print land in the middle of a line
somebody was typing.
('read prompt) therefore shows the prompt at a terminal and ignores it down a
pipe. an object program says the same thing either way.
Gate
(say (call '(host gate) '(listen (tcp "127.0.0.1:0"))))
(say (attempt (lambda () (call '(host gate) '(dial (carrier "pigeon"))))))
127.0.0.1:43731
('throw 'no-transport 'carrier "pigeon")
listen answers the address it bound, since port 0 says you do not care
which and you still have to be told. what a dial answers is
Distribution.
Anything else
(say (attempt (lambda () (call '(host stdout) '(frobnicate)))))
(say (attempt (lambda () (call '(host nope) '(anything)))))
('throw 'bad-request 'frobnicate)
('throw 'callee-down ('host 'nope) 'throw . 'noproc)
a peer refuses an unknown request the way a task does, and an unregistered name fails like a task that is not there.
monitor works on a peer too:
(monitor '(host nope))
(say (receive))
('task-down ('host 'nope) 'throw . 'noproc)