feat(v4.0.0): CONSOLE-TX capture register on the single-node model
EMIT is a device service: a character sent to the console node. The mesh and its ports are development step 2 and the memory map is open (D-4), so the single-node model now stands in for the console with one memory-mapped register, so that printing words can be run and their output compared with v3's. v4_node_console_attach(n, addr): after it, a store to word address `addr` appends the low 8 bits of the value to a buffer on the node (V4_CONSOLE_CAP characters, 4096 by default) and does not write memory; a load from `addr` reads the memory word as before. Characters past the capacity are counted in console_dropped and discarded. The hook is in v4_node_store, which all four store opcodes use. It is off by default: v4_node_reset detaches the console (address -1) and empties the capture, so the ISA's behaviour and every existing test are unchanged unless a console is attached. The address is the caller's choice. New test v4/tests/test_console.c, 22 checks per width: direct stores, !b, ! and !+ through the executor printing "Hi!", memory at and around the register untouched, the low byte only, overflow, detach, a console at word 0, and reset. At 32- and 64-bit cells, optimised and ASan+UBSan (`make test`, `make sanitize`); all other v4 tests still pass. Four mutations of the hook each fail. DECOMPOSITION.md section 7 gains the CONSOLE-TX row. No printing word is defined here; EMIT and the words on it are still to do. Test results on the amd64 host only. This is a development check, not acceptance (JUSTIFICATION.md section 16). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7a21f07ad5
commit
7e3932a4ad
@@ -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. |
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user