feat(v4.0.0): CONSOLE-RX and CONSOLE-STATUS on the single-node model

The console's receive side, standing in for the console node until the
mesh exists, as CONSOLE-TX does for output.  A data fetch (@, @+, @b)
from CONSOLE-STATUS gives -1 when a character is pending and 0 when
not; from CONSOLE-RX it gives the next character and takes it, or -1
with none pending.  The characters come from a queue the test feeds.

Instruction words and literals are still fetched with v4_node_load, so
code at a register's address is never taken for the register.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-03 20:53:56 -04:00
co-authored by Claude Opus 5.5
parent 03d8d991ef
commit 2c16183788
5 changed files with 208 additions and 7 deletions
+3 -2
View File
@@ -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.
+42
View File
@@ -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 */
+3 -3
View File
@@ -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));
+40
View File
@@ -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;
+120 -2
View File
@@ -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;