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>
245 lines
12 KiB
C
245 lines
12 KiB
C
/* 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
|
|
* 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 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;
|
|
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, w_in;
|
|
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");
|
|
|
|
/* ---- 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;
|
|
}
|