Files
LithosAnanake/v4/tests/test_stack.c
T
rajamesandClaude Opus 5.5 0da7e32a0b feat(v4.0.0): the stacks are guarded (D-16); DEPTH, PICK and ROLL
Ruled 2026-10-04, revising D-2: stack overflow and underflow are errors
that are shown and return to the prompt, not silent wrap-around.

- Each stack counts what it holds.  Before every opcode the executor
  checks that the stacks hold what it takes and have room for what it
  leaves; otherwise the opcode does nothing and the node faults, as for a
  bad address, to that kind's handler.  Every fault empties both stacks.
- The fault handler is now a table of five jumps: address, data overflow,
  data underflow, return overflow, return underflow.  The host node says
  "Stack overflow", "Stack underflow", "Return stack overflow",
  "Return stack underflow", then ERROR and the prompt.
- Two registers, DSTACK-DEPTH and RSTACK-DEPTH: a fetch reads the depth,
  a store empties the stack.  QUIT, ABORT and the error exits empty the
  return stack before they call anything; ABORT empties the data stack.
- capsule/forth.v4: DEPTH, PICK and ROLL, to FORTH-79 (counting from
  one).  PICK and ROLL set the values above the one wanted aside in
  memory, and work with the stack full.
- Division by zero now takes its operands off the stack, as v3 does.
- A colon with no room for its entry abandons the line.
- tests: every opcode at every depth of both stacks; the faults, the
  registers and the three words from the prompt.

The sizes are unchanged: ten values, nine return entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:31:18 -04:00

380 lines
13 KiB
C

/* test_stack.c -- the F18 circular stacks, checked against an independent
* model of the same semantics.
*
* The implementation in stack.c is specified in DECOMPOSITION.md as
* "T, S + 8 circular" / "R + 8 circular". This test does not take that
* decomposition on trust. It builds a second, deliberately dumb model of
* what D-2 describes -- a flat fixed-depth circular buffer with no bounds
* check at all -- and drives both with identical operation sequences,
* requiring them to agree after every single operation. If the register/ring
* bookkeeping in stack.c has a wrong pointer direction or an off-by-one in
* the wrap, this catches it; an inspection-only check would not.
*
* Depth, ordering, and the "silently overwrites the oldest entry" behaviour
* are additionally pinned with explicit cases, because those are the three
* properties the ISA's push-heavy words actually depend on.
*
* Built and run at both V4_CELL_BITS=32 and 64; see v4/Makefile.
*/
#include "v4/stack.h"
#include <stdio.h>
#include <stdlib.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)
/* ---- the independent reference model ---------------------------------------
* A flat circular buffer of depth N with no overflow or underflow detection,
* which is exactly what D-2 describes. top indexes the newest element. */
typedef struct {
v4_cell buf[64];
unsigned top;
unsigned depth;
} flat_t;
static void flat_reset(flat_t *f, unsigned depth)
{
f->depth = depth;
f->top = 0;
for (unsigned i = 0; i < 64; i++) f->buf[i] = 0;
}
static void flat_push(flat_t *f, v4_cell x)
{
f->top = (f->top + 1u) % f->depth;
f->buf[f->top] = x;
}
static v4_cell flat_pop(flat_t *f)
{
v4_cell x = f->buf[f->top];
f->top = (f->top + f->depth - 1u) % f->depth;
return x;
}
static v4_cell flat_peek(const flat_t *f)
{
return f->buf[f->top];
}
/* ---- deterministic PRNG ---------------------------------------------------
* xorshift64, so a failure is reproducible from the seed alone. No rand(),
* whose sequence is implementation-defined and would make a failure on one
* host unreproducible on another. */
static uint64_t rng_state = 0x9E3779B97F4A7C15ull;
static uint64_t rng_next(void)
{
uint64_t x = rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
rng_state = x;
return x;
}
/* ---- tests ---------------------------------------------------------------- */
static void test_reset_is_zero(void)
{
v4_dstack d;
v4_rstack r;
v4_dstack_reset(&d);
v4_rstack_reset(&r);
CHECK(v4_dstack_peek(&d) == 0, "data stack peek after reset != 0");
CHECK(v4_dstack_peek2(&d) == 0, "data stack peek2 after reset != 0");
CHECK(v4_rstack_peek(&r) == 0, "return stack peek after reset != 0");
}
static void test_exact_depth_data(void)
{
/* Fill to exactly V4_DATA_DEPTH, then drain and confirm the order. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < V4_DATA_DEPTH; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_dstack_push(&d, v);
flat_push(&f, v);
}
CHECK(v4_dstack_peek(&d) == (v4_cell)V4_DATA_DEPTH,
"peek at full depth should be the last pushed value (%d)",
(int)V4_DATA_DEPTH);
for (unsigned i = V4_DATA_DEPTH; i > 0; i--) {
v4_cell got = v4_dstack_pop(&d);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "drain at depth %u: got %lld want %lld", i,
(long long)got, (long long)exp);
CHECK(got == (v4_cell)i, "drain at depth %u: got %lld want %d", i,
(long long)got, (int)i);
}
}
static void test_overflow_overwrites_oldest(void)
{
/* D-2: "pushing past the bottom silently overwrites the oldest entry."
* Push one past depth: the very first value pushed must be gone, and the
* remaining V4_DATA_DEPTH-1 must come back newest-first. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < V4_DATA_DEPTH + 1u; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_dstack_push(&d, v);
flat_push(&f, v);
}
CHECK(v4_dstack_peek(&d) == (v4_cell)(V4_DATA_DEPTH + 1u),
"peek after overflow should be the newest value");
for (unsigned i = 0; i < V4_DATA_DEPTH; i++) {
v4_cell got = v4_dstack_pop(&d);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "post-overflow drain %u: got %lld want %lld", i,
(long long)got, (long long)exp);
CHECK(got != 1,
"post-overflow drain %u: value 1 should have been overwritten",
i);
}
}
static void test_exact_depth_return(void)
{
v4_rstack r;
flat_t f;
v4_rstack_reset(&r);
flat_reset(&f, V4_RET_DEPTH);
for (unsigned i = 0; i < V4_RET_DEPTH + 3u; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_rstack_push(&r, v);
flat_push(&f, v);
}
for (unsigned i = 0; i < V4_RET_DEPTH; i++) {
v4_cell got = v4_rstack_pop(&r);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "return drain %u: got %lld want %lld", i,
(long long)got, (long long)exp);
}
}
static void test_underflow_wraps_rather_than_trapping(void)
{
/* D-2 says there is no underflow detection. Popping an "empty" stack must
* therefore return a defined (stale) value, not fault and not abort. The
* golden model must diverge from hardware in neither direction, so this is
* asserted rather than left to chance. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < 5; i++) {
v4_dstack_pop(&d);
flat_pop(&f);
}
CHECK(1, "popping past empty must not trap");
}
/* ---- live-depth-aware comparison ------------------------------------------
* D-2 specifies what these stacks do while they hold live entries: LIFO order
* within the depth, and oldest-entry-overwritten past it. It explicitly does
* NOT specify the contents once the stack has been popped empty, because
* "no overflow or underflow" means the residue is whatever the physical
* register file happened to hold. Two independent models of a circular buffer
* will legitimately disagree down there -- it is not a semantic difference.
*
* So the differential test tracks how many live entries each stack holds and
* asserts agreement only where the spec makes a claim: the value a pop returns
* and the top peek while depth > 0, and the second element while depth > 1.
* The stale region is still exercised (the sequence runs right through it) and
* is still required not to trap, it is simply not required to agree. */
typedef struct {
v4_dstack hw;
flat_t model;
int live;
} pair_t;
static void pair_reset(pair_t *p)
{
v4_dstack_reset(&p->hw);
flat_reset(&p->model, V4_DATA_DEPTH);
p->live = 0;
}
static void pair_step(pair_t *p, int push, v4_cell v, int step, const char *tag)
{
if (push) {
v4_dstack_push(&p->hw, v);
flat_push(&p->model, v);
if (p->live < V4_DATA_DEPTH) p->live++;
} else {
v4_cell got = v4_dstack_pop(&p->hw);
v4_cell exp = flat_pop(&p->model);
if (p->live > 0) {
CHECK(got == exp, "%s op %d: pop got %lld want %lld (live=%d)",
tag, step, (long long)got, (long long)exp, p->live);
p->live--;
}
}
if (p->live > 0) {
CHECK(v4_dstack_peek(&p->hw) == flat_peek(&p->model),
"%s op %d: peek mismatch (live=%d)", tag, step, p->live);
}
if (p->live > 1) {
/* Second element of a depth-N circular buffer whose top is at `top` is
* N-1 further along the fill direction, i.e. (top-1) mod N. */
v4_cell want = p->model.buf[(p->model.top + p->model.depth - 1u)
% p->model.depth];
CHECK(v4_dstack_peek2(&p->hw) == want,
"%s op %d: peek2 got %lld want %lld (live=%d)", tag, step,
(long long)v4_dstack_peek2(&p->hw), (long long)want, p->live);
}
}
static void test_exhaustive_sequences(void)
{
/* Every push/pop sequence up to length 10 -- 2046 of them -- driven through
* both models. Exhaustive over short sequences rather than random, because
* a wrap-direction bug shows up in a handful of specific short patterns
* (notably push x N+1 then pop x N, which is the only sequence that ever
* overwrites the oldest entry) and a random driver finds those only by
* luck. This finds all of them, every run, deterministically. */
enum { MAXLEN = 10 };
int total = 0;
for (int len = 1; len <= MAXLEN; len++) total += 1 << len;
for (int len = 1; len <= MAXLEN; len++) {
for (int bits = 0; bits < (1 << len); bits++) {
pair_t p;
pair_reset(&p);
for (int i = 0; i < len; i++) {
int push = (bits >> i) & 1;
/* Distinct, non-zero values so a slot mix-up is visible. */
v4_cell v = (v4_cell)(100 + i * 7);
pair_step(&p, push, v, i, "exhaustive");
}
}
}
printf(" exhaustive: %d sequences of length <= %d\n", total, MAXLEN);
}
static void test_differential_random(void)
{
/* Long random walk across the wrap points, both widths, both stacks. */
enum { OPS = 200000 };
pair_t dp;
v4_rstack r;
flat_t rf;
int rlive;
pair_reset(&dp);
v4_rstack_reset(&r);
flat_reset(&rf, V4_RET_DEPTH);
rlive = 0;
for (int i = 0; i < OPS; i++) {
uint64_t bits = rng_next();
pair_step(&dp, (int)(bits & 1u), (v4_cell)bits, i, "differential data");
if (bits & 2u) {
v4_cell v = (v4_cell)(bits >> 8);
v4_rstack_push(&r, v);
flat_push(&rf, v);
if (rlive < V4_RET_DEPTH) rlive++;
} else {
v4_cell got = v4_rstack_pop(&r);
v4_cell exp = flat_pop(&rf);
if (rlive > 0) {
CHECK(got == exp,
"differential ret op %d: pop got %lld want %lld (live=%d)",
i, (long long)got, (long long)exp, rlive);
rlive--;
}
}
if (rlive > 0) {
CHECK(v4_rstack_peek(&r) == flat_peek(&rf),
"differential ret op %d: peek mismatch (live=%d)", i, rlive);
}
}
printf(" differential: %d ops\n", OPS);
}
/* D-16: each stack counts what it holds. The count stops at the stack's size
* and at zero; push and pop go on doing what they always did there, and it is
* the executor that turns those two cases into faults (test_exec.c). */
static void test_depth_count(void)
{
v4_dstack d;
v4_rstack r;
unsigned i;
v4_dstack_reset(&d);
v4_rstack_reset(&r);
CHECK(d.depth == 0 && r.depth == 0, "reset stacks hold nothing");
for (i = 1; i <= V4_DATA_DEPTH + 3u; i++) {
v4_dstack_push(&d, (v4_cell)i);
CHECK(d.depth == (i < V4_DATA_DEPTH ? i : (unsigned)V4_DATA_DEPTH), "data depth after %u pushes is %u", i, d.depth);
}
for (i = V4_DATA_DEPTH; i-- > 0; ) {
(void)v4_dstack_pop(&d);
CHECK(d.depth == i, "data depth counts down to %u", i);
}
(void)v4_dstack_pop(&d);
CHECK(d.depth == 0, "and stays at zero");
for (i = 1; i <= V4_RET_DEPTH + 3u; i++) {
v4_rstack_push(&r, (v4_cell)i);
CHECK(r.depth == (i < V4_RET_DEPTH ? i : (unsigned)V4_RET_DEPTH), "return depth after %u pushes is %u", i, r.depth);
}
for (i = V4_RET_DEPTH; i-- > 0; ) {
(void)v4_rstack_pop(&r);
CHECK(r.depth == i, "return depth counts down to %u", i);
}
(void)v4_rstack_pop(&r);
CHECK(r.depth == 0, "and stays at zero");
v4_dstack_push(&d, 5); v4_dstack_push(&d, 6); v4_rstack_push(&r, 7);
v4_dstack_clear(&d); v4_rstack_clear(&r);
CHECK(d.depth == 0 && r.depth == 0, "clear empties a stack");
CHECK(d.t == 6 && d.s == 5 && r.r == 7, "and changes nothing else");
CHECK(v4_dstack_guards_intact(&d) && v4_rstack_guards_intact(&r), "guards intact");
}
int main(void)
{
printf("v4 stack tests: V4_CELL_BITS=%d, data depth %d, return depth %d\n",
V4_CELL_BITS, V4_DATA_DEPTH, V4_RET_DEPTH);
test_reset_is_zero();
test_exact_depth_data();
test_overflow_overwrites_oldest();
test_exact_depth_return();
test_underflow_wraps_rather_than_trapping();
test_exhaustive_sequences();
test_differential_random();
test_depth_count();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}