diff --git a/v4/README.md b/v4/README.md index df8a231a..2a67cf01 100644 --- a/v4/README.md +++ b/v4/README.md @@ -14,8 +14,9 @@ and every ISA, hosted and bare metal, must still reach its `ok` prompt. `make -C v4 test` passing is a development check, not acceptance. What exists so far is the single node: registers, circular stacks, memory, all -32 opcodes, per-opcode and per-call-target heat, and a console capture -register (`CONSOLE-TX`) standing in for the console node until the mesh exists. `make -C v4 test` builds +32 opcodes, per-opcode and per-call-target heat, and three console +registers (`CONSOLE-TX`, which captures output, and `CONSOLE-RX` and `CONSOLE-STATUS`, which hand out +input a test feeds) standing in for the console node until the mesh exists. `make -C v4 test` builds and runs the tests at both cell widths; `make -C v4 sanitize` repeats them under ASan and UBSan. There is no compiler capsule, no POST and no K measurement yet. diff --git a/v4/include/v4/node.h b/v4/include/v4/node.h index b8cee3e1..1b0336cf 100644 --- a/v4/include/v4/node.h +++ b/v4/include/v4/node.h @@ -72,6 +72,14 @@ typedef struct { unsigned console_len; /* characters captured */ unsigned console_dropped; /* characters that did not fit */ unsigned char console[V4_CONSOLE_CAP]; + + /* CONSOLE-RX and CONSOLE-STATUS (section 7). See + * v4_node_console_input_attach below. */ + v4_cell console_rx; /* its word address, or -1: no input */ + v4_cell console_status; /* its word address, or -1 */ + unsigned input_len; /* characters fed */ + unsigned input_pos; /* characters taken; input_len - input_pos are pending */ + unsigned char input[V4_CONSOLE_CAP]; } v4_node; /* Zero P, A and B, empty the stacks, and clear memory. Installs every @@ -143,4 +151,38 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value); * Passing -1 detaches. The address is the caller's choice. */ void v4_node_console_attach(v4_node *n, v4_cell addr); +/* CONSOLE-RX and CONSOLE-STATUS, the console's receive side, on the + * single-node model. + * + * KEY and ?TERMINAL are device services too (DECOMPOSITION.md 5.10), and for + * the same reason as CONSOLE-TX the model stands in for the console with two + * memory-mapped registers and a queue of characters the test feeds. + * + * After v4_node_console_input_attach(n, rx, status), a data fetch (the + * opcodes @, @+ and @b, which go through v4_node_fetch) from word address + * `status` gives -1 when a character is pending and 0 when none is; + * `rx` gives the next pending character, 0 .. 255, and takes it off + * the queue; with none pending it gives -1 and takes nothing. + * Fetching the status changes nothing. Neither fetch reads or writes + * n->mem, a store to either address is an ordinary memory store, and + * v4_node_load -- which the executor uses to fetch instruction words and + * literals, and the assembler to patch them -- always reads memory, so code + * and literals are never mistaken for the registers. + * + * v4_node_console_feed appends characters to the queue and returns how many + * it took; the queue holds V4_CONSOLE_CAP characters that have not been + * read. A node has no input until it is attached: v4_node_reset detaches it + * (both addresses -1) and empties the queue. Attaching empties it too. + * Passing -1 for both detaches. The addresses are the caller's choice and + * must differ from each other and from CONSOLE-TX. + * + * Nothing here blocks. A KEY that must wait polls CONSOLE-STATUS, so on + * this model a wait is a loop that runs until the test feeds a character. */ +void v4_node_console_input_attach(v4_node *n, v4_cell rx, v4_cell status); +unsigned v4_node_console_feed(v4_node *n, const void *chars, unsigned len); + +/* A data fetch: what @, @+ and @b read at `addr`. The two receive registers + * above when attached, memory otherwise. Same precondition as v4_node_load. */ +v4_cell v4_node_fetch(v4_node *n, v4_cell addr); + #endif /* V4_NODE_H */ diff --git a/v4/src/exec.c b/v4/src/exec.c index 8c634d50..6f1b383d 100644 --- a/v4/src/exec.c +++ b/v4/src/exec.c @@ -91,14 +91,14 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h, n->p = add_wrap(n->p, 1); break; case V4_OP_FETCH_INC: - v4_dstack_push(ds, v4_node_load(n, n->a)); + v4_dstack_push(ds, v4_node_fetch(n, n->a)); n->a = add_wrap(n->a, 1); break; case V4_OP_FETCH_B: - v4_dstack_push(ds, v4_node_load(n, n->b)); + v4_dstack_push(ds, v4_node_fetch(n, n->b)); break; case V4_OP_FETCH_A: - v4_dstack_push(ds, v4_node_load(n, n->a)); + v4_dstack_push(ds, v4_node_fetch(n, n->a)); break; case V4_OP_STORE_P: v4_node_store(n, n->p, v4_dstack_pop(ds)); diff --git a/v4/src/node.c b/v4/src/node.c index 52788c69..24d0f4ff 100644 --- a/v4/src/node.c +++ b/v4/src/node.c @@ -12,6 +12,7 @@ void v4_node_reset(v4_node *n) v4_dstack_reset(&n->ds); v4_rstack_reset(&n->rs); v4_node_console_attach(n, -1); + v4_node_console_input_attach(n, -1, -1); } int v4_node_guards_intact(const v4_node *n) @@ -44,6 +45,45 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value) n->mem[(unsigned)addr] = value; } +v4_cell v4_node_fetch(v4_node *n, v4_cell addr) +{ + /* console_rx and console_status are -1 when no input is attached, which + * no valid address equals. */ + if (addr == n->console_status) + return n->input_pos < n->input_len ? (v4_cell)-1 : (v4_cell)0; + if (addr == n->console_rx) { + v4_cell c; + if (n->input_pos >= n->input_len) return (v4_cell)-1; + c = (v4_cell)n->input[n->input_pos++]; + if (n->input_pos == n->input_len) n->input_pos = n->input_len = 0; + return c; + } + return v4_node_load(n, addr); +} + +void v4_node_console_input_attach(v4_node *n, v4_cell rx, v4_cell status) +{ + n->console_rx = rx; + n->console_status = status; + n->input_len = 0; + n->input_pos = 0; +} + +unsigned v4_node_console_feed(v4_node *n, const void *chars, unsigned len) +{ + const unsigned char *c = (const unsigned char *)chars; + unsigned i, pending = n->input_len - n->input_pos, took = 0; + + /* Slide what is pending to the front so the whole buffer is usable. */ + if (n->input_pos > 0) { + for (i = 0; i < pending; i++) n->input[i] = n->input[n->input_pos + i]; + n->input_pos = 0; + n->input_len = pending; + } + while (took < len && n->input_len < V4_CONSOLE_CAP) n->input[n->input_len++] = c[took++]; + return took; +} + void v4_node_console_attach(v4_node *n, v4_cell addr) { n->console_tx = addr; diff --git a/v4/tests/test_console.c b/v4/tests/test_console.c index 35d4ecde..652d0656 100644 --- a/v4/tests/test_console.c +++ b/v4/tests/test_console.c @@ -1,4 +1,6 @@ -/* test_console.c -- the CONSOLE-TX capture register (node.h). +/* test_console.c -- the console registers of the single-node model (node.h): + * CONSOLE-TX, which captures what is printed, and CONSOLE-RX and + * CONSOLE-STATUS, which hand out characters the test feeds. * * EMIT is a device service (DECOMPOSITION.md 5.10) and the mesh is not built * yet, so the single-node model stands in for the console with one @@ -16,6 +18,8 @@ static int failures = 0, checks = 0; /* The memory map is open (D-4); the test chooses the address. */ #define CONSOLE_TX ((v4_cell)(V4_NODE_WORDS - 4u)) +#define CONSOLE_RX ((v4_cell)(V4_NODE_WORDS - 6u)) +#define CONSOLE_ST ((v4_cell)(V4_NODE_WORDS - 7u)) #define MARK ((v4_cell)0x1234) static v4_node n; @@ -37,7 +41,7 @@ static int run(v4_cell word) int main(void) { - v4_cell w_hi; + v4_cell w_hi, w_in; unsigned i; printf("v4 console tests: V4_CELL_BITS=%d, capture %u characters\n", @@ -114,12 +118,126 @@ int main(void) v4_node_store(&n, 0, 'z'); CHECK(n.console_len == 1 && n.console[0] == 'z' && n.mem[0] == MARK, "a console at word 0"); + /* ---- the receive side: CONSOLE-RX and CONSOLE-STATUS ---- */ + + /* Not attached: both addresses are ordinary memory to a fetch. */ + v4_node_reset(&n); + CHECK(n.console_rx == -1 && n.console_status == -1 && n.input_len == 0 && n.input_pos == 0, + "reset leaves no input attached"); + n.mem[CONSOLE_RX] = MARK; n.mem[CONSOLE_ST] = MARK + 1; + CHECK(v4_node_fetch(&n, CONSOLE_RX) == MARK && v4_node_fetch(&n, CONSOLE_ST) == MARK + 1, + "with no input attached a fetch reads memory"); + CHECK(v4_node_console_feed(&n, "x", 1) == 1 && v4_node_fetch(&n, CONSOLE_RX) == MARK, + "and fed characters reach nobody"); + + /* Attached and empty. */ + v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST); + CHECK(n.input_len == 0, "attaching empties the queue"); + CHECK(v4_node_fetch(&n, CONSOLE_ST) == 0, "status is 0 with nothing pending"); + CHECK(v4_node_fetch(&n, CONSOLE_RX) == -1 && v4_node_fetch(&n, CONSOLE_RX) == -1, + "an empty receive register reads -1"); + CHECK(n.mem[CONSOLE_RX] == MARK && n.mem[CONSOLE_ST] == MARK + 1, "the fetches left memory alone"); + CHECK(v4_node_load(&n, CONSOLE_RX) == MARK && v4_node_load(&n, CONSOLE_ST) == MARK + 1, + "v4_node_load still reads the memory words"); + + /* Fed: status says so and does not consume; each receive takes one. */ + CHECK(v4_node_console_feed(&n, "Hi", 2) == 2, "two characters fed"); + CHECK(v4_node_fetch(&n, CONSOLE_ST) == -1 && v4_node_fetch(&n, CONSOLE_ST) == -1, "status is -1, however often it is read"); + CHECK(v4_node_fetch(&n, CONSOLE_RX) == 'H', "the first character"); + CHECK(v4_node_fetch(&n, CONSOLE_ST) == -1, "one still pending"); + CHECK(v4_node_fetch(&n, CONSOLE_RX) == 'i', "the second character"); + CHECK(v4_node_fetch(&n, CONSOLE_ST) == 0 && v4_node_fetch(&n, CONSOLE_RX) == -1, "then nothing"); + + /* Every byte comes back as 0 .. 255, never negative. */ + for (i = 0; i < 256; i++) { + unsigned char c = (unsigned char)i; + CHECK(v4_node_console_feed(&n, &c, 1) == 1 && v4_node_fetch(&n, CONSOLE_RX) == (v4_cell)i, "byte %u", i); + } + + /* A store to either address is a memory store and feeds nothing. */ + v4_node_store(&n, CONSOLE_RX, 'q'); + v4_node_store(&n, CONSOLE_ST, 'r'); + CHECK(n.mem[CONSOLE_RX] == 'q' && n.mem[CONSOLE_ST] == 'r' && v4_node_fetch(&n, CONSOLE_ST) == 0, + "stores to the receive registers go to memory"); + n.mem[CONSOLE_RX] = MARK; n.mem[CONSOLE_ST] = MARK + 1; + + /* Through the executor: @b, @ and @+ each reach the registers. */ + v4_asm_begin(&as, &n, 64); + w_in = v4_asm_label(&as); + LIT(CONSOLE_ST); O(BANG_B); O(FETCH_B); /* @b status */ + LIT(CONSOLE_RX); O(BANG_A); O(FETCH_A); /* @ a character */ + LIT(CONSOLE_RX); O(BANG_A); O(FETCH_INC); /* @+ another */ + LIT(CONSOLE_ST); O(BANG_A); O(FETCH_A); /* @ status again */ + O(SEMI); + CHECK(v4_asm_ok(&as), "input test word assembles"); + CHECK(v4_node_console_feed(&n, "ok", 2) == 2, "fed"); + CHECK(run(w_in), "the word returns"); + CHECK(v4_dstack_pop(&n.ds) == 0 && v4_dstack_pop(&n.ds) == 'k' && v4_dstack_pop(&n.ds) == 'o' + && v4_dstack_pop(&n.ds) == -1, "@b, @ and @+ read status, 'o', 'k', status"); + CHECK(n.mem[CONSOLE_RX] == MARK && n.mem[CONSOLE_ST] == MARK + 1, "memory at the registers is untouched"); + + /* The code itself is never taken for a register: a literal equal to a + * register's address, and code placed at one. */ + { + v4_cell end = v4_asm_label(&as), at; + v4_asm w2; + v4_cell w_lit; + /* a second word: ( -- 'o' 77 ) CONSOLE-RX a! @ 77 ; */ + v4_asm_begin(&w2, &n, 128); + w_lit = v4_asm_label(&w2); + v4_asm_lit(&w2, CONSOLE_RX); v4_asm_op(&w2, V4_OP_BANG_A); v4_asm_op(&w2, V4_OP_FETCH_A); + v4_asm_lit(&w2, 77); v4_asm_op(&w2, V4_OP_SEMI); + CHECK(v4_asm_ok(&w2), "second input test word assembles"); + end = v4_asm_label(&w2); + /* put the status register on each word of that code in turn: the + * instruction words and both literals are still read as memory */ + for (at = 128; at < end; at++) { + v4_node_console_input_attach(&n, CONSOLE_RX, at); + CHECK(v4_node_console_feed(&n, "o", 1) == 1 && run(w_lit) + && v4_dstack_pop(&n.ds) == 77 && v4_dstack_pop(&n.ds) == 'o', + "code and literals at a register's address are read as memory [%ld]", (long)at); + } + CHECK(end - 128 >= 3, "the word spans instruction words and literals"); + } + v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST); + + /* The queue holds V4_CONSOLE_CAP unread characters, in order. */ + { + static unsigned char big[V4_CONSOLE_CAP + 5u]; + unsigned ok = 1; + for (i = 0; i < sizeof big; i++) big[i] = (unsigned char)(i * 7u + 3u); + CHECK(v4_node_console_feed(&n, big, (unsigned)sizeof big) == V4_CONSOLE_CAP, "a full queue takes no more"); + CHECK(v4_node_console_feed(&n, "x", 1) == 0, "not even one"); + for (i = 0; i < 10; i++) if (v4_node_fetch(&n, CONSOLE_RX) != big[i]) ok = 0; + CHECK(ok, "the first ten come out in order"); + CHECK(v4_node_console_feed(&n, big, 20) == 10, "ten read makes room for ten"); + for (i = 10; i < V4_CONSOLE_CAP; i++) if (v4_node_fetch(&n, CONSOLE_RX) != big[i]) ok = 0; + for (i = 0; i < 10; i++) if (v4_node_fetch(&n, CONSOLE_RX) != big[i]) ok = 0; + CHECK(ok && v4_node_fetch(&n, CONSOLE_ST) == 0, "and everything comes out in the order it went in"); + } + + /* Output and input do not disturb each other. */ + v4_node_console_attach(&n, CONSOLE_TX); + CHECK(v4_node_console_feed(&n, "A", 1) == 1, "fed"); + v4_node_store(&n, CONSOLE_TX, 'B'); + CHECK(v4_node_fetch(&n, CONSOLE_RX) == 'A' && n.console_len == 1 && n.console[0] == 'B', + "the transmit and receive sides are separate"); + + /* Detaching returns both addresses to memory. */ + v4_node_console_input_attach(&n, -1, -1); + CHECK(v4_node_fetch(&n, CONSOLE_RX) == MARK && v4_node_fetch(&n, CONSOLE_ST) == MARK + 1, + "detached, both addresses are memory again"); + CHECK(v4_node_guards_intact(&n), "guards intact"); /* Reset detaches and empties. */ + v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST); + (void)v4_node_console_feed(&n, "left over", 9); v4_node_reset(&n); CHECK(n.console_tx == -1 && n.console_len == 0 && n.console_dropped == 0, "reset detaches the console and empties the capture"); + CHECK(n.console_rx == -1 && n.console_status == -1 && n.input_len == 0 && n.input_pos == 0, + "reset detaches the input and empties the queue"); printf(" %d checks, %d failures\n", checks, failures); return failures ? 1 : 0;