Files
LithosAnanake/v4/tests/test_node.c
T
rajamesandClaude Opus 5.5 c8dc14897c feat(v4.0.0): block storage, LOAD and LIST (D-19)
- The golden model's host node gets a block storage device: four
  memory-mapped registers (number, address, command, status), 1024-byte
  blocks as 256 cells.  tests/test_node.c.
- capsule/blocks.v4: BLOCK BUFFER UPDATE SAVE-BUFFERS EMPTY-BUFFERS LIST
  LOAD SCR BLK (FORTH-79) and v3's FLUSH THRU and -->, over two buffers.
- The text being interpreted is at the address in (SRC), which QUERY makes
  the terminal's buffer and LOAD a block's; LOAD saves and restores it, so
  blocks nest and the rest of LOAD's line runs afterwards.  WORD makes
  sure a loading block is still in a buffer before it reads.
- In a block, \ skips to the next 64-character line.
- A block number that does not exist, and --> at the terminal, are errors
  with messages (D-18).
- tests/test_host_quit.c: ten transcripts of the v3 binary; nesting three
  deep on two buffers; what reaches the device and when.

v3's LOAD drops the rest of its line, and v3 has no BLK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:32:04 -04:00

327 lines
14 KiB
C

/* test_node.c -- the node: registers, memory, and the boundary on memory.
*
* The memory boundary does a job the stack boundaries do not: it is the landing
* place for a *linear* index error -- mem[-1], mem[V4_NODE_WORDS] -- which is
* inside the struct and therefore invisible to AddressSanitizer.
*
* It does not cover an out-of-range *word address*, where P, A or B has left
* the address space: as an index that would land gigabytes away, outside any
* band. D-14 guards that with a range check, tested here for the C accessors
* and in test_exec.c for a running programme.
*/
#include "v4/node.h"
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" FAIL %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static void test_geometry(void)
{
/* The boundary is 5% of memory at each end, rounded up, never zero. */
CHECK(V4_MEM_BOUND == V4_GUARD_ELEMS(V4_NODE_WORDS),
"memory boundary is not 5%% of %u words", V4_NODE_WORDS);
CHECK(V4_MEM_BOUND >= 1u, "memory boundary must be at least one word");
CHECK((unsigned long)V4_MEM_BOUND * 100u
>= (unsigned long)V4_NODE_WORDS * V4_GUARD_PCT,
"memory boundary %u is less than %u%% of %u", V4_MEM_BOUND,
V4_GUARD_PCT, V4_NODE_WORDS);
/* At the default size that is a real cost, and naming it here means the
* number is on the record rather than discovered from a memory report. */
printf(" node: %u words + %u head + %u tail boundary "
"(%.1f%% overhead)\n",
V4_NODE_WORDS, V4_MEM_BOUND, V4_MEM_BOUND,
100.0 * 2.0 * (double)V4_MEM_BOUND / (double)V4_NODE_WORDS);
}
static void test_reset_state(void)
{
v4_node n;
v4_node_reset(&n);
CHECK(n.p == 0, "P after reset");
CHECK(n.a == 0, "A after reset");
CHECK(n.b == 0, "B after reset");
CHECK(v4_node_guards_intact(&n), "all boundaries intact after reset");
/* T, S and R live in the stacks, so they are checked through the stack
* API rather than as node fields. */
CHECK(v4_dstack_peek(&n.ds) == 0, "T after reset");
CHECK(v4_dstack_peek2(&n.ds) == 0, "S after reset");
CHECK(v4_rstack_peek(&n.rs) == 0, "R after reset");
}
static void test_guards_absent_before_reset(void)
{
/* A node that has never been reset holds whatever was on the stack. The
* check must notice, which is what proves it reads the boundary rather than
* returning a constant. */
v4_node *n = (v4_node *)malloc(sizeof *n);
if (n == NULL) {
failures++;
printf(" FAIL %s:%d: out of memory\n", __FILE__, __LINE__);
return;
}
memset(n, 0xA5, sizeof *n);
CHECK(!v4_node_guards_intact(n),
"boundaries must not read as intact before reset");
v4_node_reset(n);
CHECK(v4_node_guards_intact(n), "boundaries intact after reset");
free(n);
}
static void test_load_store_roundtrip(void)
{
v4_node n;
v4_node_reset(&n);
/* Every word, not a sample: a stray boundary in the middle of memory would
* not be found by spot checks. */
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
v4_node_store(&n, i, i * 3 + 1);
}
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
CHECK(v4_node_load(&n, i) == i * 3 + 1,
"word %lld: got %lld want %lld", (long long)i,
(long long)v4_node_load(&n, i), (long long)(i * 3 + 1));
}
CHECK(v4_node_guards_intact(&n), "memory boundary survived a full sweep");
}
static void test_load_store_edges(void)
{
/* The first and last words, which are the ones adjacent to the boundary.
* A boundary that is a word too wide would quietly steal word 0 or the
* last word, and the memory would still pass every interior test. */
v4_node n;
v4_node_reset(&n);
v4_node_store(&n, 0, 0x11111111);
v4_node_store(&n, (v4_cell)V4_NODE_WORDS - 1, 0x22222222);
CHECK(v4_node_load(&n, 0) == 0x11111111, "word 0 did not take its value");
CHECK(v4_node_load(&n, (v4_cell)V4_NODE_WORDS - 1) == 0x22222222,
"last word did not take its value");
CHECK(v4_node_guards_intact(&n), "edge writes disturbed the boundary");
}
static void test_boundary_is_adjacent_to_memory(void)
{
/* The band only catches a linear index error if it is immediately next to
* mem, so adjacency is asserted rather than assumed -- the compiler is free
* to reorder struct members, and a band that drifted to the far end of the
* struct would silently stop catching anything. */
CHECK(offsetof(v4_node, mem_guard_tail)
== offsetof(v4_node, mem) + sizeof(((v4_node *)0)->mem),
"tail boundary does not start immediately after memory");
CHECK(offsetof(v4_node, mem_guard_head)
+ sizeof(((v4_node *)0)->mem_guard_head)
== offsetof(v4_node, mem),
"head boundary does not end immediately before memory");
}
static void test_linear_overrun_low_is_caught(void)
{
/* A linear index error -- the kind an off-by-one in a loop bound or a
* pointer step produces. It lands in the head boundary, it is inside the
* struct so AddressSanitizer says nothing about it, and the boundary check
* is what reports it. Written through the boundary array rather than as
* mem[-1], because mem[-1] is out of bounds of a declared array and the
* standard lets the compiler do anything with it; the offsetof test above
* establishes that this *is* the word mem[-1] resolves to. */
v4_node n;
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact before the deliberate overrun");
n.mem_guard_head[V4_MEM_BOUND - 1u] = (v4_cell)0xDEADBEEF;
CHECK(!v4_node_guards_intact(&n),
"a linear access before mem was not detected");
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
}
static void test_linear_overrun_high_is_caught(void)
{
v4_node n;
v4_node_reset(&n);
n.mem_guard_tail[0] = (v4_cell)0xDEADBEEF;
CHECK(!v4_node_guards_intact(&n),
"a linear access past the last word was not detected");
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
}
static void test_out_of_range_word_address_touches_nothing(void)
{
/* A word address outside memory is a different thing from a linear index
* error: -1 as an index would be 2^32 words past mem, far outside the
* struct and any band. D-14 (2026-10-04) rules it guarded: load gives 0
* and store does nothing. (That a running programme faults there is the
* executor's business: test_exec.c.) */
static const v4_cell bad[] = { -1, -2, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + (v4_cell)V4_MEM_BOUND,
(v4_cell)V4_MSB, (v4_cell)(V4_MSB - 1u) };
v4_node *n = malloc(sizeof *n);
unsigned i;
if (!n) { CHECK(0, "malloc"); return; }
v4_node_reset(n);
for (i = 0; i < V4_NODE_WORDS; i++) n->mem[i] = (v4_cell)(i + 1000u);
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
CHECK(!v4_node_addr_ok(bad[i]), "%lld is not an address", (long long)bad[i]);
CHECK(v4_node_load(n, bad[i]) == 0, "a load at %lld gives 0", (long long)bad[i]);
v4_node_store(n, bad[i], 0x5A5A);
}
for (i = 0; i < V4_NODE_WORDS; i++)
if (n->mem[i] != (v4_cell)(i + 1000u)) { CHECK(0, "a store outside memory changed word %u", i); break; }
CHECK(v4_node_guards_intact(n), "and the boundaries are intact");
CHECK(v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)), "the first and last words are addresses");
free(n);
}
/* D-19: the block storage device. Four registers; a store to the third
* reads a block into 256 cells of memory or writes it from them. */
static void test_storage_device(void)
{
static unsigned char blocks[4 * V4_BLOCK_BYTES];
const v4_cell REG = 40, BUF = 100;
v4_node *n = malloc(sizeof *n);
unsigned i;
int ok;
if (!n) { CHECK(0, "malloc"); return; }
v4_node_reset(n);
CHECK(n->storage_reg == -1, "a fresh node has no storage");
for (i = 0; i < sizeof blocks; i++) blocks[i] = (unsigned char)(i * 7u + (i >> 10));
v4_node_storage_attach(n, REG, blocks, 4);
/* read block 2 */
n->mem[BUF - 1] = 111; n->mem[BUF + 256] = 222;
v4_node_store(n, REG, 2); v4_node_store(n, REG + 1, BUF);
n->mem[REG + 3] = 55;
v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == 0, "a read that works leaves status 0");
for (ok = 1, i = 0; i < V4_BLOCK_CELLS; i++) {
const unsigned char *b = blocks + 2 * V4_BLOCK_BYTES + 4u * i;
v4_ucell want = (v4_ucell)b[0] | ((v4_ucell)b[1] << 8) | ((v4_ucell)b[2] << 16) | ((v4_ucell)b[3] << 24);
if ((v4_ucell)n->mem[BUF + (v4_cell)i] != want) ok = 0;
}
CHECK(ok, "the block is in memory, four bytes to a cell, the first lowest, the rest of the cell zero");
CHECK(n->mem[BUF - 1] == 111 && n->mem[BUF + 256] == 222, "and only 256 cells were written");
CHECK(n->mem[REG] == 2 && n->mem[REG + 1] == BUF && n->mem[REG + 2] == 1, "the registers are memory words");
/* write it to block 0, changed */
n->mem[BUF] = (v4_cell)0x44332211; n->mem[BUF + 255] = (v4_cell)(v4_ucell)0xAABBCCDDu;
v4_node_store(n, REG, 0);
v4_node_store(n, REG + 2, 2);
CHECK(n->mem[REG + 3] == 0 && blocks[0] == 0x11 && blocks[1] == 0x22 && blocks[2] == 0x33 && blocks[3] == 0x44
&& blocks[1020] == 0xDD && blocks[1023] == 0xAA, "a write puts the low four bytes of each cell on the device");
CHECK(memcmp(blocks + 4, blocks + 2 * V4_BLOCK_BYTES + 4, 1016) == 0, "the rest of the block as it was read");
CHECK(blocks[V4_BLOCK_BYTES] == (unsigned char)(1024u * 7u + 1u), "and the next block untouched");
/* what does not work: status -1, nothing moved */
v4_node_store(n, REG, 4); v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == -1, "a block past the last");
v4_node_store(n, REG, -1); v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == -1, "a negative block");
v4_node_store(n, REG, 1); v4_node_store(n, REG + 1, (v4_cell)V4_NODE_WORDS - 255); v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == -1, "cells that run past the end of memory");
v4_node_store(n, REG + 1, -4); v4_node_store(n, REG + 2, 2);
CHECK(n->mem[REG + 3] == -1, "a negative address");
v4_node_store(n, REG + 1, BUF); v4_node_store(n, REG + 2, 3);
CHECK(n->mem[REG + 3] == -1, "an unknown command");
v4_node_store(n, REG + 1, (v4_cell)V4_NODE_WORDS - 256); v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == 0 && v4_node_guards_intact(n), "the last 256 cells of memory are usable");
/* detached: plain memory */
v4_node_storage_attach(n, -1, 0, 0);
n->mem[REG + 3] = 9; v4_node_store(n, REG + 2, 1);
CHECK(n->mem[REG + 3] == 9 && n->mem[REG + 2] == 1, "detached, the registers are memory");
v4_node_storage_attach(n, REG, blocks, 4);
v4_node_reset(n);
CHECK(n->storage_reg == -1, "reset detaches the device");
free(n);
}
static void test_ends_are_distinguishable(void)
{
/* The two boundaries carry different patterns precisely so that a failure
* says which end. Checked here on memory as well as in test_guard.c on the
* ring, because "which end" is the property that makes a report actionable. */
v4_node n;
v4_node_reset(&n);
CHECK((v4_ucell)n.mem_guard_head[0] == V4_GUARD_PATTERN_HEAD,
"memory head boundary pattern");
CHECK((v4_ucell)n.mem_guard_tail[0] == V4_GUARD_PATTERN_TAIL,
"memory tail boundary pattern");
CHECK(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL,
"head and tail patterns are equal at V4_CELL_BITS=%d", V4_CELL_BITS);
}
static void test_workload_never_false_alarms(void)
{
/* A boundary that fires on legitimate use is worse than no boundary. Drive
* a workload across the whole address space, through both address registers
* and the program counter, and require every boundary to survive. */
v4_node n;
v4_node_reset(&n);
uint64_t s = 0xB5026F5AA96619E9ull;
for (int i = 0; i < 200000; i++) {
s ^= s << 13; s ^= s >> 7; s ^= s << 17;
v4_cell addr = (v4_cell)(s % (uint64_t)V4_NODE_WORDS);
v4_node_store(&n, addr, (v4_cell)s);
n.a = addr;
n.b = (v4_cell)((addr + 1u) % V4_NODE_WORDS);
n.p = (v4_cell)((addr + 2u) % V4_NODE_WORDS);
v4_dstack_push(&n.ds, (v4_cell)s);
v4_rstack_push(&n.rs, (v4_cell)(s >> 13));
if (i & 1) { (void)v4_dstack_pop(&n.ds); (void)v4_rstack_pop(&n.rs); }
}
CHECK(v4_node_guards_intact(&n),
"a boundary fired during ordinary workload");
CHECK(n.p < (v4_cell)V4_NODE_WORDS, "P left the address space");
CHECK(n.a < (v4_cell)V4_NODE_WORDS, "A left the address space");
CHECK(n.b < (v4_cell)V4_NODE_WORDS, "B left the address space");
}
int main(void)
{
printf("v4 node tests: V4_CELL_BITS=%d, %u words\n",
V4_CELL_BITS, V4_NODE_WORDS);
test_geometry();
test_reset_state();
test_guards_absent_before_reset();
test_load_store_roundtrip();
test_load_store_edges();
test_boundary_is_adjacent_to_memory();
test_linear_overrun_low_is_caught();
test_linear_overrun_high_is_caught();
test_out_of_range_word_address_touches_nothing();
test_storage_device();
test_ends_are_distinguishable();
test_workload_never_false_alarms();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}