Files
LithosAnanake/v4/tests/test_console.c
T
rajamesandClaude Opus 5.5 7e3932a4ad 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>
2026-10-03 11:57:49 -04:00

127 lines
5.2 KiB
C

/* 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;
}