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>
339 lines
12 KiB
C
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;
|
|
}
|