Files
LithosAnanake/v4/tests/test_guard.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

214 lines
8.2 KiB
C

/* test_guard.c -- the 5% safety boundary.
*
* A guard band whose only assertion is "the patterns still look right" has not
* been shown to catch anything. These tests therefore do three things a
* structural test cannot:
*
* 1. pin the sizing arithmetic, including the case that makes a 5% margin
* evaporate -- a short list, where 5% rounds down to zero elements and a
* zero-width guard is a comment rather than a boundary;
* 2. index past each end of a real stack on purpose, and require the check to
* fail, and require it to say *which* end was hit;
* 3. run the real push/pop paths and require the boundary to survive them, so
* a boundary that fires on ordinary use is caught as a false alarm rather
* than being tuned into uselessness.
*/
#include "v4/guard.h"
#include "v4/stack.h"
#include <stdint.h>
#include <stdio.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_sizing_rounds_up(void)
{
/* 5% of each, rounded up. */
CHECK(V4_GUARD_ELEMS(20) == 1, "5%% of 20 is 1, got %u", V4_GUARD_ELEMS(20));
CHECK(V4_GUARD_ELEMS(40) == 2, "5%% of 40 is 2, got %u", V4_GUARD_ELEMS(40));
CHECK(V4_GUARD_ELEMS(100) == 5, "5%% of 100 is 5, got %u", V4_GUARD_ELEMS(100));
CHECK(V4_GUARD_ELEMS(1000) == 50, "5%% of 1000 is 50, got %u", V4_GUARD_ELEMS(1000));
CHECK(V4_GUARD_ELEMS(2000) == 100, "5%% of 2000 is 100, got %u", V4_GUARD_ELEMS(2000));
}
static void test_sizing_never_zero(void)
{
/* The case that matters. 5% of anything under 20 rounds to 0 by
* truncation, and a guard of zero elements catches nothing at all while
* still reading as "5% boundary" in the source. */
for (unsigned n = 1; n < 20u; n++) {
unsigned g = V4_GUARD_ELEMS(n);
CHECK(g >= 1u, "a %u-element list got a %u-element boundary", n, g);
}
CHECK(V4_GUARD_ELEMS(1) == 1, "1-element list boundary");
CHECK(V4_GUARD_ELEMS(3) == 1, "3-element list boundary");
CHECK(V4_GUARD_ELEMS(8) == 1, "8-element list boundary");
CHECK(V4_GUARD_ELEMS(19) == 1, "19-element list boundary");
CHECK(V4_GUARD_ELEMS(V4_DATA_RING) == 1, "data ring boundary at depth 8");
CHECK(V4_GUARD_ELEMS(V4_RET_RING) == 1, "return ring boundary at depth 8");
}
static void test_patterns_are_distinct_at_this_width(void)
{
/* The compile-time assertion in guard.h covers this, but the point of
* distinct patterns is "which end", and that is only meaningful if it
* survives truncation to the cell width. */
CHECK(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL,
"head and tail patterns are equal at V4_CELL_BITS=%d", V4_CELL_BITS);
CHECK((v4_ucell)V4_GUARD_PATTERN_HEAD != 0u, "head pattern truncated to 0");
CHECK((v4_ucell)V4_GUARD_PATTERN_TAIL != 0u, "tail pattern truncated to 0");
}
static void test_fill_and_intact(void)
{
v4_cell band[7];
v4_guard_fill(band, 7, V4_GUARD_PATTERN_HEAD);
CHECK(v4_guard_intact(band, 7, V4_GUARD_PATTERN_HEAD), "filled band intact");
CHECK(!v4_guard_intact(band, 7, V4_GUARD_PATTERN_TAIL),
"a head-filled band must not read as a tail band");
/* Every element is checked, not just the ends. */
for (unsigned i = 0; i < 7; i++) {
band[i] = 0;
CHECK(!v4_guard_intact(band, 7, V4_GUARD_PATTERN_HEAD),
"band intact after corrupting interior element %u", i);
v4_guard_fill(band, 7, V4_GUARD_PATTERN_HEAD);
}
}
static void test_reset_installs_boundaries(void)
{
v4_dstack d;
v4_rstack r;
/* A struct that has never been reset holds whatever was on the stack, so
* the boundary cannot be assumed present. Poison both first: that the
* check fails before reset is the point, since it is what proves the
* check is reading the boundary and not returning a constant. */
memset(&d, 0xA5, sizeof d);
memset(&r, 0xA5, sizeof r);
CHECK(!v4_dstack_guards_intact(&d),
"data boundaries must not read as intact before reset");
CHECK(!v4_rstack_guards_intact(&r),
"return boundaries must not read as intact before reset");
v4_dstack_reset(&d);
v4_rstack_reset(&r);
CHECK(v4_dstack_guards_intact(&d), "data boundaries intact after reset");
CHECK(v4_rstack_guards_intact(&r), "return boundaries intact after reset");
}
static void test_head_overrun_is_detected(void)
{
/* Deliberately index one element before the ring: exactly what a lost
* modulo or a sign error on the head arithmetic would do. */
v4_dstack d;
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact before the deliberate overrun");
d.guard_head[0] = (v4_cell)V4_GUARD_PATTERN_TAIL; /* wrong pattern too */
CHECK(!v4_dstack_guards_intact(&d),
"head overrun past the data ring was not detected");
/* Which end is answerable because the two patterns differ. */
CHECK((v4_ucell)d.guard_head[0] != V4_GUARD_PATTERN_HEAD,
"head boundary no longer holds its own pattern");
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact after re-reset");
}
static void test_tail_overrun_is_detected(void)
{
v4_dstack d;
v4_dstack_reset(&d);
d.guard_tail[0] = 0;
CHECK(!v4_dstack_guards_intact(&d),
"tail overrun past the data ring was not detected");
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact after re-reset");
}
static void test_return_stack_boundaries(void)
{
v4_rstack r;
v4_rstack_reset(&r);
CHECK(v4_rstack_guards_intact(&r), "return boundaries intact after reset");
r.guard_head[0] = 0;
CHECK(!v4_rstack_guards_intact(&r), "return head overrun not detected");
v4_rstack_reset(&r);
r.guard_tail[V4_RET_BOUND - 1u] = 0;
CHECK(!v4_rstack_guards_intact(&r), "return tail overrun not detected");
v4_rstack_reset(&r);
CHECK(v4_rstack_guards_intact(&r), "return intact after re-reset");
}
static void test_ordinary_use_never_touches_the_boundaries(void)
{
/* The other half of the contract: a boundary that fires on legitimate use
* is worse than none, because it trains you to ignore it. Drive both
* stacks hard, well past their depths, and require the boundaries to
* survive every operation. */
v4_dstack d;
v4_rstack r;
v4_dstack_reset(&d);
v4_rstack_reset(&r);
uint64_t s = 0x243F6A8885A308D3ull;
for (int i = 0; i < 50000; i++) {
s ^= s << 13; s ^= s >> 7; s ^= s << 17;
v4_dstack_push(&d, (v4_cell)s);
v4_rstack_push(&r, (v4_cell)(s >> 11));
if (i & 1) {
(void)v4_dstack_pop(&d);
(void)v4_rstack_pop(&r);
}
}
CHECK(v4_dstack_guards_intact(&d),
"data boundary damaged by ordinary push/pop");
CHECK(v4_rstack_guards_intact(&r),
"return boundary damaged by ordinary push/pop");
/* head must still be in range, which is the invariant the boundary exists
* to police. */
CHECK(d.head < V4_DATA_RING, "data head %u out of range", d.head);
CHECK(r.head < V4_RET_RING, "return head %u out of range", r.head);
}
int main(void)
{
printf("v4 guard tests: V4_CELL_BITS=%d, data bound %d, return bound %d\n",
V4_CELL_BITS, V4_DATA_BOUND, V4_RET_BOUND);
test_sizing_rounds_up();
test_sizing_never_zero();
test_patterns_are_distinct_at_this_width();
test_fill_and_intact();
test_reset_installs_boundaries();
test_head_overrun_is_detected();
test_tail_overrun_is_detected();
test_return_stack_boundaries();
test_ordinary_use_never_touches_the_boundaries();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}