diff --git a/docs/v4.0.0/DECOMPOSITION.md b/docs/v4.0.0/DECOMPOSITION.md index 4d5446b3..930b4179 100644 --- a/docs/v4.0.0/DECOMPOSITION.md +++ b/docs/v4.0.0/DECOMPOSITION.md @@ -813,3 +813,4 @@ Addresses are assigned in the node memory map (D-4). Names only here. | `REC-ENABLE` | R/W | DoE recorder on/off (`HB-ON` / `HB-OFF`). | | `PORT-STATUS` | R | Per-port ready flags, for non-blocking polls. | | `NODE-ERROR` | R/W | Arithmetic error flag: set to −1 by `Q./` on division by zero (D-11), by `Q.SQRT` / `Q.LOG` outside their domain (D-12) and by `HOLD` on a bad character or a full buffer (D-13), cleared by writing 0. `VM-ERROR?` reads it. | +| `CONSOLE-TX` | W | Console transmit: a store sends the low 8 bits of the value as one character. This is what `EMIT` writes to. On the single-node golden model it is a capture register: the character is appended to a buffer on the node and memory is not written (`v4_node_console_attach`, `v4/include/v4/node.h`), so printing words can be run and their output compared with v3's. With the mesh (step 2) it becomes the port to the console node. | diff --git a/v4/README.md b/v4/README.md index 5315f636..df8a231a 100644 --- a/v4/README.md +++ b/v4/README.md @@ -14,7 +14,8 @@ 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, and per-opcode and per-call-target heat. `make -C v4 test` builds +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 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 f794c30d..b8cee3e1 100644 --- a/v4/include/v4/node.h +++ b/v4/include/v4/node.h @@ -49,6 +49,11 @@ * build time, which is why the percentage is a macro and not baked in. */ #define V4_MEM_BOUND V4_GUARD_ELEMS(V4_NODE_WORDS) +/* Characters the console capture holds. Overridable at build time. */ +#ifndef V4_CONSOLE_CAP +#define V4_CONSOLE_CAP 4096u +#endif + typedef struct { v4_cell p; /* P -- program counter */ v4_cell a; /* A -- address register */ @@ -60,6 +65,13 @@ typedef struct { v4_cell mem_guard_head[V4_MEM_BOUND]; v4_cell mem[V4_NODE_WORDS]; v4_cell mem_guard_tail[V4_MEM_BOUND]; + + /* CONSOLE-TX capture (DECOMPOSITION.md section 7). See + * v4_node_console_attach below. */ + v4_cell console_tx; /* its word address, or -1: no console */ + unsigned console_len; /* characters captured */ + unsigned console_dropped; /* characters that did not fit */ + unsigned char console[V4_CONSOLE_CAP]; } v4_node; /* Zero P, A and B, empty the stacks, and clear memory. Installs every @@ -111,4 +123,24 @@ int v4_node_guards_intact(const v4_node *n); v4_cell v4_node_load(const v4_node *n, v4_cell addr); void v4_node_store(v4_node *n, v4_cell addr, v4_cell value); +/* CONSOLE-TX, the console's transmit register, on the single-node model. + * + * EMIT is a device service: one character sent to the console node + * (DECOMPOSITION.md 5.10). The mesh and its ports are development step 2, + * and the memory map is open (D-4), so until then the model stands in for + * the console with one memory-mapped register and a capture buffer. That is + * enough to run every printing word and compare what it prints with v3. + * + * After v4_node_console_attach(n, addr), a store to word address `addr` + * appends the low 8 bits of the value to n->console instead of writing + * memory; n->mem[addr] is never changed by it, and a load from `addr` reads + * that memory word as before. All four store opcodes go through + * v4_node_store, so all four reach it. Once V4_CONSOLE_CAP characters are + * held, further ones are counted in n->console_dropped and discarded. + * + * A node has no console until one is attached: v4_node_reset detaches it + * (console_tx = -1) and empties the capture. Attaching empties it too. + * Passing -1 detaches. The address is the caller's choice. */ +void v4_node_console_attach(v4_node *n, v4_cell addr); + #endif /* V4_NODE_H */ diff --git a/v4/src/node.c b/v4/src/node.c index d654f181..52788c69 100644 --- a/v4/src/node.c +++ b/v4/src/node.c @@ -11,6 +11,7 @@ void v4_node_reset(v4_node *n) v4_guard_fill(n->mem_guard_tail, V4_MEM_BOUND, V4_GUARD_PATTERN_TAIL); v4_dstack_reset(&n->ds); v4_rstack_reset(&n->rs); + v4_node_console_attach(n, -1); } int v4_node_guards_intact(const v4_node *n) @@ -31,5 +32,21 @@ v4_cell v4_node_load(const v4_node *n, v4_cell addr) void v4_node_store(v4_node *n, v4_cell addr, v4_cell value) { + /* CONSOLE-TX: capture the character, leave memory alone. console_tx is + * -1 when no console is attached, which no valid address equals. */ + if (addr == n->console_tx) { + if (n->console_len < V4_CONSOLE_CAP) + n->console[n->console_len++] = (unsigned char)((v4_ucell)value & 0xFFu); + else + n->console_dropped++; + return; + } n->mem[(unsigned)addr] = value; } + +void v4_node_console_attach(v4_node *n, v4_cell addr) +{ + n->console_tx = addr; + n->console_len = 0; + n->console_dropped = 0; +} diff --git a/v4/tests/test_console.c b/v4/tests/test_console.c new file mode 100644 index 00000000..35d4ecde --- /dev/null +++ b/v4/tests/test_console.c @@ -0,0 +1,126 @@ +/* test_console.c -- the CONSOLE-TX capture register (node.h). + * + * 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 + * memory-mapped register: a store to it appends a character to a buffer on + * the node. This file checks that register and nothing else: no printing + * word is defined here. + */ +#include "v4/asm.h" +#include "v4/testcode.h" +#include +#include + +static int failures = 0, checks = 0; +#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0) + +/* The memory map is open (D-4); the test chooses the address. */ +#define CONSOLE_TX ((v4_cell)(V4_NODE_WORDS - 4u)) +#define MARK ((v4_cell)0x1234) + +static v4_node n; +static v4_exec_state es; +static v4_heat h; +static v4_asm as; + +#define O(name) v4_asm_op(&as, V4_OP_##name) +#define LIT(v) v4_asm_lit(&as, (v4_cell)(v)) + +static int run(v4_cell word) +{ + v4_dstack_reset(&n.ds); + v4_rstack_reset(&n.rs); + v4_exec_reset(&es); + v4_heat_reset(&h); + return v4_test_call(&n, &es, &h, word, 1000) > 0; +} + +int main(void) +{ + v4_cell w_hi; + unsigned i; + + printf("v4 console tests: V4_CELL_BITS=%d, capture %u characters\n", + V4_CELL_BITS, (unsigned)V4_CONSOLE_CAP); + + /* A fresh node has no console: every store is a memory store. */ + v4_node_reset(&n); + CHECK(n.console_tx == -1 && n.console_len == 0 && n.console_dropped == 0, + "reset leaves no console attached"); + v4_node_store(&n, CONSOLE_TX, MARK); + CHECK(v4_node_load(&n, CONSOLE_TX) == MARK && n.console_len == 0, + "with no console the address is ordinary memory"); + + /* Attached: a store is captured and memory is left alone. */ + v4_node_console_attach(&n, CONSOLE_TX); + v4_node_store(&n, CONSOLE_TX, 'A'); + CHECK(n.console_len == 1 && n.console[0] == 'A', "a store is captured"); + CHECK(n.mem[CONSOLE_TX] == MARK, "and does not write memory"); + CHECK(v4_node_load(&n, CONSOLE_TX) == MARK, "a load reads the memory word, as before"); + + /* Only the low byte is the character. */ + v4_node_store(&n, CONSOLE_TX, 0x141); + v4_node_store(&n, CONSOLE_TX, -1); + v4_node_store(&n, CONSOLE_TX, 0); + CHECK(n.console_len == 4 && n.console[1] == 'A' && n.console[2] == 0xFF && n.console[3] == 0, + "the low 8 bits are the character"); + + /* Other addresses are still memory. */ + v4_node_store(&n, CONSOLE_TX - 1, 'x'); + v4_node_store(&n, CONSOLE_TX + 1, 'y'); + CHECK(n.console_len == 4 && n.mem[CONSOLE_TX - 1] == 'x' && n.mem[CONSOLE_TX + 1] == 'y', + "neighbouring addresses are ordinary memory"); + + /* Through the executor: !b, ! and !+ each reach the register, in order. */ + v4_asm_begin(&as, &n, 16); + w_hi = v4_asm_label(&as); + LIT('H'); LIT(CONSOLE_TX); O(BANG_B); O(STORE_B); /* !b */ + LIT('i'); LIT(CONSOLE_TX); O(BANG_A); O(STORE_A); /* ! */ + LIT('!'); LIT(CONSOLE_TX); O(BANG_A); O(STORE_INC); /* !+ */ + O(SEMI); + CHECK(v4_asm_ok(&as), "test word assembles"); + + n.mem[CONSOLE_TX - 1] = 0; + n.mem[CONSOLE_TX + 1] = 0; + v4_node_console_attach(&n, CONSOLE_TX); + CHECK(n.console_len == 0 && n.console_dropped == 0, "attaching empties the capture"); + CHECK(run(w_hi), "the word returns"); + CHECK(n.console_len == 3 && memcmp(n.console, "Hi!", 3) == 0, "!b, ! and !+ print \"Hi!\""); + CHECK(n.a == CONSOLE_TX + 1, "!+ still advances A"); + CHECK(n.mem[CONSOLE_TX] == MARK && n.mem[CONSOLE_TX - 1] == 0 && n.mem[CONSOLE_TX + 1] == 0, + "memory at and around the register is untouched"); + + /* Run it again: the capture appends. */ + CHECK(run(w_hi) && n.console_len == 6 && memcmp(n.console, "Hi!Hi!", 6) == 0, + "a second run appends"); + + /* A full buffer drops and counts; nothing past it is written. */ + v4_node_console_attach(&n, CONSOLE_TX); + for (i = 0; i < V4_CONSOLE_CAP + 5u; i++) v4_node_store(&n, CONSOLE_TX, (v4_cell)('a' + i % 26u)); + CHECK(n.console_len == V4_CONSOLE_CAP, "the capture stops at its capacity"); + CHECK(n.console_dropped == 5, "and counts what it dropped: %u", n.console_dropped); + CHECK(n.console[0] == 'a' && n.console[V4_CONSOLE_CAP - 1] == (unsigned char)('a' + (V4_CONSOLE_CAP - 1u) % 26u), + "the first and last captured characters are intact"); + CHECK(n.console_tx == CONSOLE_TX, "the register address survives an overflow"); + + /* Detaching returns the address to memory. */ + v4_node_console_attach(&n, -1); + v4_node_store(&n, CONSOLE_TX, 77); + CHECK(n.console_len == 0 && n.mem[CONSOLE_TX] == 77, "detached, the address is memory again"); + + /* The console can sit anywhere, including word 0. */ + v4_node_console_attach(&n, 0); + n.mem[0] = MARK; + v4_node_store(&n, 0, 'z'); + CHECK(n.console_len == 1 && n.console[0] == 'z' && n.mem[0] == MARK, "a console at word 0"); + + CHECK(v4_node_guards_intact(&n), "guards intact"); + + /* Reset detaches and empties. */ + 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"); + + printf(" %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +}