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>
214 lines
8.2 KiB
C
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;
|
|
}
|