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