Files
LithosAnanake/v4/tests/test_stack.c
T
rajamesandClaude Opus 5.5 067f317c47 feat(v4.0.0): hosted golden model, single node
First code for StarForth v4 (JUSTIFICATION.md section 10, step 1): one node
of the 32-instruction core as a C99 model, with cell width as a build
parameter.

- Node: P, A, B, F18 circular stacks (10 and 9 deep, D-2), word-addressed
  memory (D-1), 5% guard bands on every bounded list.
- Instruction word: six 5-bit slots in 32 bits at every cell width.
- Executor: all 32 opcodes of DECOMPOSITION.md 1.3. Cell arithmetic wraps
  explicitly; no signed overflow or implementation-defined shift.
- Heat: per-opcode and per-call-target counters and the anti-clock, driven
  by instruction retirement (1.4, D-6 interim).
- Slot packer and runner for tests, and a reference unsigned multiply in
  plain C99 with no 128-bit type.

Tests run at 32- and 64-bit cells, and under ASan and UBSan. They cover
every opcode and execute the first section 4 definitions (NIP SWAP OR
NEGATE ROT 0< 0= 2DUP - U<) against the C operation each stands for.

UM* as written in section 4 is exact only while u1 <= 2^(n-2). Two known
failing cases are pinned in test_foundation.c until it is rewritten.

DECOMPOSITION.md: record D-9, the instruction word is 32 bits at every
cell width (ruled 2026-10-02).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:38:27 -04:00

339 lines
12 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);
}
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();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}