17 Standard host modules

(1) The modules of this clause are host words that come with the processor. They are not part of the language: they exist only when a host registers them (13.3). pnx registers all six. A host that embeds the library picks them one by one with pinax_host_register(state, "io") and the like (library libpinax_host). Using a module that was not registered is refused (13.1).

(2) Host words keep the same rules as other words: their signatures are checked at load, words with or fail shall be guarded, and owned values are handed over exactly once.

(3) What the operating system refuses is a failure (or fail). A missing file and the end of an input are err.not_found, bytes of the wrong form err.format, no room err.full, and any other refusal (permission, a device gone) err.failed. A word without or fail (print, close) does not report the operating system’s refusal.

17.1 io — standard input and output

(1)

WordSignatureMeaning
print println(s str)writes to standard output; println adds a line break (LF)
eprint(s str)writes to standard error
read_line(gives line str or fail)one line of standard input, without its line break; err.not_found at the end of input
read_line_into(buf buffer or fail)appends one line to a buffer

Bytes are written unchanged; the same bytes come out on every operating system.

17-io.pnx

use io
use str
word demo ()
  "no new line, " io.print
  "then one" io.println
  loop
    io.read_line or_else (e) break end to line         rem fails at the end of the input
    cat ("read: ", line, " (", line str.len, " bytes)") io.println
  end
end
demo

Output

$ pnx 17-io.pnx
no new line, then one
read: first line (10 bytes)
read: second line (11 bytes)

17.2 file — files

(1) The handle type file is an open file. The opening words give an owned file and close takes it (12.4).

WordSignatureMeaning
open_read open_write(path str gives owned file or fail)opens for reading; for writing (made new or emptied)
write(s str, f file or fail)writes bytes
read_line(f file gives line str or fail)one line; err.not_found at the end
read(f file, buf buffer gives n int or fail)reads into a buffer and gives the number of bytes read
read_line_into(f file, buf buffer or fail)appends one line to a buffer
read_all(path str gives body str or fail)reads a whole file into the region
read_all_into(path str, buf buffer or fail)appends a whole file to a buffer
close(f owned file)closes it

A missing file is err.not_found.

17-file.pnx

use io
use file
buffer buf 64
word demo (or fail)
  "data.txt" file.open_write or_fail to out
  "alpha\nbeta\n" out file.write or_else (e) out file.close e fail end
  out file.close
  "data.txt" file.open_read or_fail to in
  loop
    in file.read_line or_else (e) break end to line
    cat ("line: ", line) io.println
  end
  in file.close
  "data.txt" buf file.read_all_into or_fail
  cat ("all: ", buf contents) io.print
end
demo or_else (e) cat ("file error: ", e err_text) io.println end
"missing.txt" file.read_all or_else (e) cat ("missing.txt: ", e err_text) end io.println

Output

$ pnx 17-file.pnx
line: alpha
line: beta
all: alpha
beta
missing.txt: not_found

17.3 clock random env

(1)

WordSignatureMeaning
clock.now_ms(gives int)the time now, in milliseconds since 1970-01-01 UTC
clock.monotonic_ns(gives int)nanoseconds of a clock that never goes back
clock.sleep(ms int)waits; the state does nothing else meanwhile
random.seed(n int)sets the sequence; the same seed gives the same sequence
random.random(gives int)a 64-bit random number (not for cryptography)
random.random_below(n int gives int)from 0 to n−1
env.arg_count(gives int)the number of arguments; 0 is the program file
env.arg(i int gives s str or fail)argument i
env.getenv(name str gives s str or fail)an environment variable; err.not_found when unset

17-env.pnx

use io
use env
cat ("arguments: ", env.arg_count) io.println
0 env.arg_count range do (i int) i env.arg or_else (e) "?" end io.println end each
"PINAX_SURELY_UNSET" env.getenv or_else (e) cat ("not set: ", e err_text) end io.println

Output

$ pnx 17-env.pnx alpha beta
arguments: 3
17-env.pnx
alpha
beta
not set: not_found

17-random-clock.pnx

use io
use random
use clock
word demo ()
  42 random.seed                                       rem the same seed, the same numbers
  6 random.random_below to a
  a 0 ge a 6 lt and if "a die in 0..5" io.println end
  clock.monotonic_ns to t0
  clock.monotonic_ns to t1
  t1 t0 ge if "the monotonic clock does not go back" io.println end
end
demo

Output

$ pnx 17-random-clock.pnx
a die in 0..5
the monotonic clock does not go back

17.4 Channels and the host loop

(1) A channel is a line of byte messages, up to a set number of them. The host names a channel and registers it in a state as a module of that name (pinax_channel_register, library libpinax_host). The states that registered the same channel pass messages to each other. They share no memory: a message is copied in and copied out.

WordSignatureMeaning
send(msg str or fail)puts a message in; waits while the channel is full
receive(gives msg str or fail)takes the message that came first; waits while it is empty
close()no more messages go in

(2) send fails with err.full when the message is longer than the channel allows, and with err.cancelled once the channel is closed. receive fails with err.not_found on a channel that is closed and empty. Messages left in a closed channel are still taken.

(3) Only the task that called waits (14.2); the other tasks of the state go on. Only runs the host loop runs (pinax_loop_start) wait. In any other run, a send or receive that would have to wait ends the run with the fault PINAX_ERR_STATE.

(4) The host loop runs states on a set number of threads. pinax_loop_open takes a configuration: the number of threads, how many runs it holds at once, how much a host operation’s results may take, and one block of memory, as a state takes one: the loop’s objects are fixed slots in it (pinax_loop_size bytes), and each channel carves its slots from the same block when it is opened (pinax_channel_size bytes). Nothing is allocated while runs go on; with no block the loop takes one piece of that size from proven’s heap. A state runs on one thread at a time. A run whose budget (19.5) is used up goes to the back of the line, and a run that waits gives its thread up. While the host waits in pinax_loop_wait, when every run left waits for a channel and no host operation is outstanding, the loop ends all of them with PINAX_ERR_DEADLOCK.

17-channel.c

#include "pinax.h"
#include "pinax_loop.h"
#include <stdio.h>
#include <string.h>

/* One state sends the numbers 1 to 100 through the channel `jobs`; two others take them and
   add them up. The loop runs the three on two threads. */
static const char producer[] =
    "use jobs\nuse str\n"
    "word produce (n int gives r int)\n"
    "  1 to i\n"
    "  loop\n"
    "    i n gt until\n"
    "    i str.to_str jobs.send or_else (e) -1 return end\n"
    "    i 1 add to i\n"
    "  end\n"
    "  jobs.close\n"
    "  n\n"
    "end\n";
static const char consumer[] =
    "use jobs\nuse str\n"
    "word consume (n int gives r int)\n"
    "  0 to total\n"
    "  loop\n"
    "    jobs.receive or_else (e) \"\" end to m      rem not_found: closed and empty\n"
    "    m str.is_empty until\n"
    "    m str.to_int or_else (e) 0 end total add to total\n"
    "  end\n"
    "  total\n"
    "end\n";

typedef struct worker {
    unsigned char block[1 << 20];
    pinax_state *state;
    pinax_word word;
    int64_t in, out;
    pinax_args args;
    pinax_err outcome;
} worker;
static worker w[3];

static void done(void *user, pinax_state *state, pinax_err outcome) {
    (void)state;
    ((worker *)user)->outcome = outcome;
}

static int start(pinax_loop *loop, pinax_channel *jobs, worker *x, const char *source, const char *word) {
    pinax_config config = {.struct_size = sizeof config, .budget = 1000};
    if (pinax_open((pinax_mem){x->block, sizeof x->block}, &config, &x->state) != PINAX_OK ||
        pinax_channel_register(x->state, jobs) != PINAX_OK ||
        pinax_load(x->state, "main", (pinax_str){(const unsigned char *)source, strlen(source)}, PINAX_LOAD_CODE) ||
        pinax_find(x->state, word, "(n int gives r int)", 0, &x->word) != PINAX_OK)
        return 1;
    x->args = (pinax_args){.i = &x->in, .ri = &x->out};
    return pinax_loop_start(loop, x->state, x->word, &x->args, done, x) != PINAX_OK;
}

int main(void) {
    // the loop and its channel live in one block the host sizes with the two size functions
    pinax_loop_config loop_config = {.struct_size = sizeof loop_config, .threads = 2, .states = 3};
    static unsigned char loop_block[1 << 16];
    if (pinax_loop_size(&loop_config) + pinax_channel_size(&loop_config, 4, 20) > sizeof loop_block)
        return 1;
    loop_config.block = (pinax_mem){loop_block, sizeof loop_block};
    pinax_loop *loop;
    pinax_channel *jobs;
    if (pinax_loop_open(&loop_config, &loop) != PINAX_OK || pinax_channel_open(loop, "jobs", 4, 20, &jobs) != PINAX_OK)
        return 1;
    w[0].in = 100;
    if (start(loop, jobs, &w[1], consumer, "consume") || start(loop, jobs, &w[2], consumer, "consume") ||
        start(loop, jobs, &w[0], producer, "produce"))
        return 1;
    pinax_loop_wait(loop);
    printf("sent %lld, received %lld\n", (long long)w[0].out, (long long)(w[1].out + w[2].out));

    /* the channel is closed and empty now: one more receiver ends at once */
    w[1].in = 0;
    if (pinax_loop_start(loop, w[1].state, w[1].word, &w[1].args, done, &w[1]) != PINAX_OK)
        return 1;
    pinax_loop_wait(loop);
    printf("after close: %lld\n", (long long)w[1].out);

    /* a receiver on a channel nobody sends to: every run left waits, so the loop ends it */
    pinax_channel *idle;
    pinax_close(w[2].state);
    if (pinax_channel_open(loop, "jobs", 1, 20, &idle) != PINAX_OK || start(loop, idle, &w[2], consumer, "consume"))
        return 1;
    pinax_loop_wait(loop);
    printf("nobody sends: %s\n", w[2].outcome == PINAX_ERR_DEADLOCK ? "deadlock" : "?");
    pinax_loop_close(loop);
    for (int k = 0; k < 3; ++k)
        pinax_close(w[k].state);
    return 0;
}

Output

$ ./17-channel
sent 100, received 5050
after close: 0
nobody sends: deadlock

17.5 sh — commands

(1) sh runs commands through the system shell (/bin/sh -c; cmd.exe /c on Windows). pnx registers it; a host that embeds the library registers it only for scripts that may run commands (pinax_host_register(state,
"sh")
; pinax_host_register_all does not), and the WebAssembly host has no sh.

WordSignatureMeaning
sh.run(cmd str gives status int or fail)runs cmd; its output goes where the program’s goes, after what the program wrote. The status is the command’s: a status other than 0 is not a failure. Fails only when no shell can be started
sh.output(cmd str gives out str or fail)runs cmd and gives what it wrote on standard output, as written (with its line breaks), in the region
sh.output_into(cmd str, buf buffer gives status int or fail)the same, appended to a buffer; err.full when it does not fit (the command is read to its end)
sh.open_read(cmd str gives owned file or fail)a pipe from the command’s standard output, as a file (17.2): file.read_line, file.read, file.close
sh.open_write(cmd str gives owned file or fail)a pipe to the command’s standard input: file.write, file.close
sh.status(gives int)the status of the last command that ended: by run, output, output_into, or a pipe’s file.close, which waits for its command
sh.quote(s str gives str)s as one word for the system shell, to build a command from data: in single quotes for /bin/sh; in double quotes for cmd.exe, where % and ! keep their meaning
sh.cd(path str or fail)changes the working directory of the process; err.not_found when it cannot
sh.cwd(gives path str or fail)the working directory
sh.setenv(name str, value str or fail)sets an environment variable for the commands that follow (and env.getenv)
sh.exit(status int)ends the program with that status (0 to 255; others are clamped), after what is held back is written. The run stops there; in pnx repl the session ends

17-sh.pnx

use io
use sh
word main (or fail)
  "echo one; echo two" sh.output or_fail io.print
  cat ("status ", "exit 4" sh.run or_fail) io.println
  cat ("echo ", "it's here" sh.quote) sh.output or_fail io.print
end
main or_else (e) cat ("failed: ", e err_text) io.println end

Output

$ pnx 17-sh.pnx
one
two
status 4
it's here

(2) A status is the command’s exit code; a command ended by a signal has 128 plus the signal’s number. A command that cannot be found is a status (127 for /bin/sh), not a failure. A command with a zero byte in it is err.argument. A write to a pipe whose command has stopped reading fails (err.failed). sh.status and sh.exit are of the thread that runs the state.