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>
This commit is contained in:
rajames
2026-10-02 14:38:27 -04:00
co-authored by Claude Opus 5.5
parent 1f98034311
commit 067f317c47
33 changed files with 3635 additions and 3 deletions
+115
View File
@@ -0,0 +1,115 @@
/* asm.c -- slot packer for test programs. See asm.h. */
#include "v4/asm.h"
static void put(v4_asm *as, unsigned op)
{
as->cur |= (v4_iword)(op & 0x1Fu) << V4_SLOT_LOW_BIT(as->slot);
as->slot++;
}
static void emit(v4_asm *as, v4_cell value)
{
if (as->here < 0 || (v4_ucell)as->here >= V4_NODE_WORDS) {
as->error = 1;
return;
}
v4_node_store(as->n, as->here, value);
as->here++;
}
/* Write out the word being built, then the literals it owes. */
static void flush(v4_asm *as)
{
if (as->slot == 0) return;
while (as->slot < V4_SLOT_COUNT) put(as, V4_OP_NOP);
emit(as, (v4_cell)as->cur);
for (unsigned i = 0; i < as->nlit; i++) emit(as, as->lit[i]);
as->cur = 0;
as->slot = 0;
as->nlit = 0;
}
/* 1 if a branch in `slot`, executing with P = p, can reach `target`. */
static int reachable(v4_cell p, unsigned slot, v4_cell target)
{
v4_ucell mask = (v4_ucell)v4_iword_slot_mask(slot);
return (((v4_ucell)p ^ (v4_ucell)target) & ~mask) == 0;
}
void v4_asm_begin(v4_asm *as, v4_node *n, v4_cell origin)
{
as->n = n;
as->here = origin;
as->cur = 0;
as->slot = 0;
as->nlit = 0;
as->error = 0;
}
void v4_asm_op(v4_asm *as, unsigned op)
{
if (op >= V4_OPCODE_COUNT || v4_op_is_branch(op) || op == V4_OP_FETCH_P) {
as->error = 1;
return;
}
put(as, op);
if (op == V4_OP_SEMI || op == V4_OP_EX || as->slot == V4_SLOT_COUNT) flush(as);
}
void v4_asm_lit(v4_asm *as, v4_cell value)
{
put(as, V4_OP_FETCH_P);
as->lit[as->nlit++] = value;
if (as->slot == V4_SLOT_COUNT) flush(as);
}
v4_asm_ref v4_asm_branch_fwd(v4_asm *as, unsigned op)
{
v4_asm_ref ref;
if (!v4_op_is_branch(op)) as->error = 1;
if (!v4_iword_branch_legal(as->slot)) flush(as);
ref.addr = as->here;
ref.slot = as->slot;
/* The word has been fetched and every literal before the branch consumed
* by the time the branch executes. */
ref.p = as->here + 1 + (v4_cell)as->nlit;
put(as, op);
as->slot = V4_SLOT_COUNT; /* the rest of the word is address */
flush(as);
return ref;
}
void v4_asm_resolve(v4_asm *as, v4_asm_ref ref, v4_cell target)
{
v4_ucell mask = (v4_ucell)v4_iword_slot_mask(ref.slot);
v4_ucell word;
if (as->error) return;
if (!reachable(ref.p, ref.slot, target)) {
as->error = 1;
return;
}
word = (v4_ucell)v4_node_load(as->n, ref.addr);
word = (word & ~mask) | ((v4_ucell)target & mask);
v4_node_store(as->n, ref.addr, (v4_cell)word);
}
void v4_asm_branch(v4_asm *as, unsigned op, v4_cell target)
{
v4_asm_resolve(as, v4_asm_branch_fwd(as, op), target);
}
v4_cell v4_asm_label(v4_asm *as)
{
flush(as);
return as->here;
}
int v4_asm_ok(v4_asm *as)
{
flush(as);
return !as->error;
}
+209
View File
@@ -0,0 +1,209 @@
/* exec.c -- instruction execution. See exec.h.
*
* All cell arithmetic is done in v4_ucell and converted back. Signed overflow
* is undefined behaviour in C99 and a right shift of a negative value is
* implementation-defined, and a golden model cannot have either: the hardware
* wraps, so the model wraps, on every host and at every optimisation level.
*/
#include "v4/exec.h"
static v4_cell add_wrap(v4_cell x, v4_cell y)
{
return (v4_cell)((v4_ucell)x + (v4_ucell)y);
}
/* Arithmetic shift right by one: the sign bit is kept. */
static v4_cell asr1(v4_cell x)
{
v4_ucell u = (v4_ucell)x;
return (v4_cell)((u >> 1) | (u & V4_MSB));
}
/* An instruction word occupies the low 32 bits of a cell at every cell width
* (DECOMPOSITION.md D-9). */
static v4_iword fetch_iword(const v4_node *n, v4_cell addr)
{
return (v4_iword)((v4_ucell)v4_node_load(n, addr) & 0xFFFFFFFFu);
}
/* Opcodes after which no further slot of the word executes. The five branch
* opcodes end it because everything to their right is address, taken or not.
* `;` and `ex` end it because P has been replaced, as on the F18. `unext` is
* not in this set: when its loop finishes it falls through to the next slot. */
static int ends_word(unsigned op)
{
return v4_op_is_branch(op) || op == V4_OP_SEMI || op == V4_OP_EX;
}
void v4_exec_reset(v4_exec_state *es)
{
es->anticlock = 0;
}
void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
unsigned op, v4_iword w, unsigned slot)
{
v4_dstack *ds = &n->ds;
v4_rstack *rs = &n->rs;
v4_cell x;
switch (op) {
case V4_OP_SEMI:
n->p = v4_rstack_pop(rs);
break;
case V4_OP_EX:
x = rs->r;
rs->r = n->p;
n->p = x;
break;
case V4_OP_JUMP:
n->p = v4_iword_branch(n->p, w, slot);
break;
case V4_OP_CALL:
x = v4_iword_branch(n->p, w, slot);
v4_rstack_push(rs, n->p);
v4_heat_on_call(h, x);
n->p = x;
break;
case V4_OP_UNEXT:
/* The restart at slot 0 is v4_exec_step_word's business; this is only
* the effect on R. */
if (rs->r != 0) rs->r = add_wrap(rs->r, -1);
else (void)v4_rstack_pop(rs);
break;
case V4_OP_NEXT:
if (rs->r != 0) {
rs->r = add_wrap(rs->r, -1);
n->p = v4_iword_branch(n->p, w, slot);
} else {
(void)v4_rstack_pop(rs);
}
break;
case V4_OP_IF: /* does not pop T */
if (ds->t == 0) n->p = v4_iword_branch(n->p, w, slot);
break;
case V4_OP_MINUS_IF: /* does not pop T */
if (((v4_ucell)ds->t & V4_MSB) == 0) n->p = v4_iword_branch(n->p, w, slot);
break;
case V4_OP_FETCH_P:
v4_dstack_push(ds, v4_node_load(n, n->p));
n->p = add_wrap(n->p, 1);
break;
case V4_OP_FETCH_INC:
v4_dstack_push(ds, v4_node_load(n, n->a));
n->a = add_wrap(n->a, 1);
break;
case V4_OP_FETCH_B:
v4_dstack_push(ds, v4_node_load(n, n->b));
break;
case V4_OP_FETCH_A:
v4_dstack_push(ds, v4_node_load(n, n->a));
break;
case V4_OP_STORE_P:
v4_node_store(n, n->p, v4_dstack_pop(ds));
n->p = add_wrap(n->p, 1);
break;
case V4_OP_STORE_INC:
v4_node_store(n, n->a, v4_dstack_pop(ds));
n->a = add_wrap(n->a, 1);
break;
case V4_OP_STORE_B:
v4_node_store(n, n->b, v4_dstack_pop(ds));
break;
case V4_OP_STORE_A:
v4_node_store(n, n->a, v4_dstack_pop(ds));
break;
case V4_OP_MUL_STEP:
/* D-3, plain F18: the carry out of T+S is not kept. T:A then shifts
* right one bit, T keeping its sign bit and T's bit 0 entering the top
* of A. S is not consumed. */
x = ds->t;
if ((v4_ucell)n->a & 1u) x = add_wrap(x, ds->s);
n->a = (v4_cell)(((v4_ucell)n->a >> 1)
| (((v4_ucell)x & 1u) << (V4_CELL_BITS - 1)));
ds->t = asr1(x);
break;
case V4_OP_TWO_STAR:
ds->t = (v4_cell)((v4_ucell)ds->t << 1);
break;
case V4_OP_TWO_SLASH:
ds->t = asr1(ds->t);
break;
case V4_OP_INV:
ds->t = (v4_cell)~(v4_ucell)ds->t;
break;
case V4_OP_ADD:
x = v4_dstack_pop(ds); /* old T; T is now old S */
ds->t = add_wrap(ds->t, x);
break;
case V4_OP_AND:
x = v4_dstack_pop(ds);
ds->t = (v4_cell)((v4_ucell)ds->t & (v4_ucell)x);
break;
case V4_OP_XOR:
x = v4_dstack_pop(ds);
ds->t = (v4_cell)((v4_ucell)ds->t ^ (v4_ucell)x);
break;
case V4_OP_DROP:
(void)v4_dstack_pop(ds);
break;
case V4_OP_DUP:
v4_dstack_push(ds, ds->t);
break;
case V4_OP_RPOP:
v4_dstack_push(ds, v4_rstack_pop(rs));
break;
case V4_OP_OVER:
v4_dstack_push(ds, ds->s);
break;
case V4_OP_PUSH_A:
v4_dstack_push(ds, n->a);
break;
case V4_OP_NOP:
break;
case V4_OP_PUSH:
v4_rstack_push(rs, v4_dstack_pop(ds));
break;
case V4_OP_BANG_B:
n->b = v4_dstack_pop(ds);
break;
case V4_OP_BANG_A:
n->a = v4_dstack_pop(ds);
break;
default:
return; /* not an opcode: nothing retires */
}
v4_heat_on_retire(h, op, &es->anticlock);
}
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
{
v4_iword iw = fetch_iword(n, n->p);
unsigned executed = 0;
unsigned slot = 0;
n->p = add_wrap(n->p, 1);
while (slot < V4_SLOT_COUNT) {
unsigned op = v4_iword_op(iw, slot);
/* Decided before the opcode runs, because running it changes R. */
int again = (op == V4_OP_UNEXT) && (n->rs.r != 0);
v4_exec_op(n, es, h, op, iw, slot);
executed++;
if (again) {
/* Same word, slot 0, no refetch and P untouched: an `@p` or `!p`
* inside a unext loop therefore walks the words that follow, as
* it does on the F18. */
slot = 0;
continue;
}
if (ends_word(op)) break;
slot++;
}
return executed;
}
+15
View File
@@ -0,0 +1,15 @@
/* guard.c -- 5% safety boundaries. See guard.h. */
#include "v4/guard.h"
void v4_guard_fill(v4_cell *band, unsigned n, v4_ucell pattern)
{
for (unsigned i = 0; i < n; i++) band[i] = (v4_cell)pattern;
}
int v4_guard_intact(const v4_cell *band, unsigned n, v4_ucell pattern)
{
for (unsigned i = 0; i < n; i++) {
if ((v4_ucell)band[i] != pattern) return 0;
}
return 1;
}
+89
View File
@@ -0,0 +1,89 @@
/* heat.c -- heat and anti-clock. See heat.h. */
#include "v4/heat.h"
#include <string.h>
void v4_heat_reset(v4_heat *h)
{
memset(h->op, 0, sizeof(h->op));
memset(h->call, 0, sizeof(h->call));
memset(h->call_freeze_mask, 0, sizeof(h->call_freeze_mask));
}
void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock)
{
if (op < V4_OPCODE_COUNT) {
h->op[op]++;
}
if (anticlock) (*anticlock)++;
}
static unsigned call_index(v4_cell target)
{
/* Index by target address. The table is a fixed-size cache for the model.
* A real hardware table is deferred; here we hash-mod or just take low bits?
* Or simply if in range use it. The spec says "per-call-target heat table"
* indexed by target. For golden model, targets are small (node memory). */
unsigned idx = (unsigned)(v4_ucell)target;
if (idx >= V4_HEAT_MAX_CALL_TARGETS) {
/* Out of range: ignore for now? Or clamp? D-6 deferred; but heat must
* be recorded for actual call targets exercised. Many tests will have
* targets < 4096. This is a conservative model choice: if target is
* outside the modeled table, we do not increment a modeled slot (and
* we could log, but not specified). The boundary philosophy says detect
* rather than corrupt: for the model, we just don't record in the fixed
* table -- acceptable until D-4/D-6 pin sizes. */
return (unsigned)-1; /* invalid */
}
return idx;
}
void v4_heat_on_call(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
if (v4_heat_call_is_frozen(h, target)) return;
h->call[idx]++;
}
v4_uheat_t v4_heat_call_get(const v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
return h->call[idx];
}
int v4_heat_call_set(v4_heat *h, v4_cell target, v4_uheat_t val)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
if (v4_heat_call_is_frozen(h, target)) return 0;
h->call[idx] = val;
return 1;
}
void v4_heat_call_freeze(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
h->call_freeze_mask[word] |= ((v4_ucell)1u << bit);
}
void v4_heat_call_unfreeze(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
h->call_freeze_mask[word] &= ~((v4_ucell)1u << bit);
}
int v4_heat_call_is_frozen(const v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
return (h->call_freeze_mask[word] & ((v4_ucell)1u << bit)) != 0;
}
+74
View File
@@ -0,0 +1,74 @@
/* iword.c -- instruction-word decode. See iword.h for the layout. */
#include "v4/iword.h"
/* The five opcodes that consume a target from the low bits of their slot
* (section 1.2): jump, call, next, if, -if. A table rather than a switch so
* that the set is visible in one place and can be counted in the tests. */
int v4_op_is_branch(unsigned op)
{
return op == V4_OP_JUMP || op == V4_OP_CALL || op == V4_OP_NEXT
|| op == V4_OP_IF || op == V4_OP_MINUS_IF;
}
int v4_iword_branch_legal(unsigned slot)
{
return slot < 4u; /* section 1.2: branches are legal in slots 0-3 only */
}
uint32_t v4_iword_slot_mask(unsigned slot)
{
if (slot >= V4_SLOT_COUNT) return 0u;
/* 27-5k address bits, e.g. 0x07FFFFFF for slot 0 down to 0x00000FFF for
* slot 3. For slots 4 and 5 the formula still produces 7 and 2 bits; the
* legality check is separate because those slots are simply not usable. */
return ((uint32_t)1u << V4_BRANCH_ADDR_BITS(slot)) - 1u;
}
unsigned v4_iword_op(v4_iword w, unsigned slot)
{
if (slot >= V4_SLOT_COUNT) return 0u;
return (unsigned)((w >> V4_SLOT_LOW_BIT(slot)) & 0x1Fu);
}
unsigned v4_iword_spare(v4_iword w)
{
return (unsigned)(w & ((1u << V4_SPARE_BITS) - 1u));
}
v4_iword v4_iword_target(v4_iword w, unsigned slot)
{
if (slot >= V4_SLOT_COUNT) return 0u;
return w & v4_iword_slot_mask(slot);
}
v4_iword v4_iword_assemble(const unsigned op[V4_SLOT_COUNT], unsigned spare)
{
v4_iword w = 0u;
for (unsigned k = 0; k < V4_SLOT_COUNT; k++) {
/* An opcode that does not fit in 5 bits is a bug in whoever built the
* word, not something to encode silently. Truncating here would let a
* malformed capsule execute as something else. */
if (op[k] > 0x1Fu) return 0xFFFFFFFFu;
w |= (v4_iword)(op[k] & 0x1Fu) << V4_SLOT_LOW_BIT(k);
}
if (spare > ((1u << V4_SPARE_BITS) - 1u)) return 0xFFFFFFFFu;
w |= (v4_iword)(spare & ((1u << V4_SPARE_BITS) - 1u));
return w;
}
v4_cell v4_iword_branch(v4_cell p, v4_iword w, unsigned slot)
{
uint32_t mask = v4_iword_slot_mask(slot);
v4_ucell up = (v4_ucell)p;
/* Page-relative, as in the F18: the target supplies the low
* V4_BRANCH_ADDR_BITS(slot) bits of P and the bits above them survive. The
* mask is computed in 32 bits and widened, so a 64-bit P keeps its high
* half -- which is the correct reading of "replaces the same low bits of P"
* at any cell width. */
up = (up & ~(v4_ucell)mask) | (v4_ucell)(w & mask);
return (v4_cell)up;
}
+35
View File
@@ -0,0 +1,35 @@
/* node.c -- the v4 node. See node.h. */
#include "v4/node.h"
void v4_node_reset(v4_node *n)
{
n->p = 0;
n->a = 0;
n->b = 0;
for (unsigned i = 0; i < V4_NODE_WORDS; i++) n->mem[i] = 0;
v4_guard_fill(n->mem_guard_head, V4_MEM_BOUND, V4_GUARD_PATTERN_HEAD);
v4_guard_fill(n->mem_guard_tail, V4_MEM_BOUND, V4_GUARD_PATTERN_TAIL);
v4_dstack_reset(&n->ds);
v4_rstack_reset(&n->rs);
}
int v4_node_guards_intact(const v4_node *n)
{
return v4_guard_intact(n->mem_guard_head, V4_MEM_BOUND, V4_GUARD_PATTERN_HEAD)
&& v4_guard_intact(n->mem_guard_tail, V4_MEM_BOUND, V4_GUARD_PATTERN_TAIL)
&& v4_dstack_guards_intact(&n->ds)
&& v4_rstack_guards_intact(&n->rs);
}
v4_cell v4_node_load(const v4_node *n, v4_cell addr)
{
/* Precondition checked by the caller; see node.h. The unsigned compare is
* deliberate: `addr` is signed, and a negative address must not wrap into
* a large positive one and read the top of memory instead. */
return n->mem[(unsigned)addr];
}
void v4_node_store(v4_node *n, v4_cell addr, v4_cell value)
{
n->mem[(unsigned)addr] = value;
}
+107
View File
@@ -0,0 +1,107 @@
/* stack.c -- F18 circular hardware stacks.
*
* The two decompositions (T/S + 8-ring, R + 8-ring) are exactly equivalent to
* flat circular buffers of depth 10 and 9 respectively. The argument, since
* the ISA is specified in terms of the decomposition and the differential
* test in tests/test_stack.c is written against the flat form:
*
* Data stack, 10 deep. Read the stack top to bottom as
*
* T, S, ring[(head-1) mod 8], ring[(head-2) mod 8], ..., ring[head mod 8]
*
* -- that is, ring[(head-1) mod 8] sits immediately below S, and head names
* the oldest slot. push(x) does ring[head]=S; S=T; T=x; head=(head+1) mod 8,
* which appends at the top and rotates the oldest entry into the ring.
* pop() takes T, promotes S, and pulls ring[(head-1) mod 8] up into S while
* stepping head back one -- so the element that was third becomes second and
* the ring is re-seated one position earlier. That is a 10-slot circular
* buffer whose slot 0 is T and slot 1 is S, and the head arithmetic is
* bookkeeping for which ring slot is oldest.
*
* Return stack, 9 deep, same argument with one register instead of two: the
* stack reads R, ring[(head-1) mod 8], ..., ring[head mod 8].
*
* "No overflow or underflow; pushing past the bottom silently overwrites the
* oldest entry" (D-2) is not an extra rule here -- it is what a fixed-depth
* circular buffer with no bounds check does on its own, once the pointer is
* allowed to wrap. Nothing below tests head against anything.
*/
#include "v4/stack.h"
void v4_dstack_reset(v4_dstack *st)
{
st->t = 0;
st->s = 0;
st->head = 0;
for (unsigned i = 0; i < V4_DATA_RING; i++) st->ring[i] = 0;
v4_guard_fill(st->guard_head, V4_DATA_BOUND, V4_GUARD_PATTERN_HEAD);
v4_guard_fill(st->guard_tail, V4_DATA_BOUND, V4_GUARD_PATTERN_TAIL);
}
int v4_dstack_guards_intact(const v4_dstack *st)
{
return v4_guard_intact(st->guard_head, V4_DATA_BOUND, V4_GUARD_PATTERN_HEAD)
&& v4_guard_intact(st->guard_tail, V4_DATA_BOUND, V4_GUARD_PATTERN_TAIL);
}
void v4_dstack_push(v4_dstack *st, v4_cell x)
{
st->ring[st->head] = st->s;
st->s = st->t;
st->t = x;
st->head = (st->head + 1u) % V4_DATA_RING;
}
v4_cell v4_dstack_pop(v4_dstack *st)
{
v4_cell x = st->t;
st->t = st->s;
st->head = (st->head + V4_DATA_RING - 1u) % V4_DATA_RING;
st->s = st->ring[st->head];
return x;
}
v4_cell v4_dstack_peek(const v4_dstack *st)
{
return st->t;
}
v4_cell v4_dstack_peek2(const v4_dstack *st)
{
return st->s;
}
void v4_rstack_reset(v4_rstack *st)
{
st->r = 0;
st->head = 0;
for (unsigned i = 0; i < V4_RET_RING; i++) st->ring[i] = 0;
v4_guard_fill(st->guard_head, V4_RET_BOUND, V4_GUARD_PATTERN_HEAD);
v4_guard_fill(st->guard_tail, V4_RET_BOUND, V4_GUARD_PATTERN_TAIL);
}
int v4_rstack_guards_intact(const v4_rstack *st)
{
return v4_guard_intact(st->guard_head, V4_RET_BOUND, V4_GUARD_PATTERN_HEAD)
&& v4_guard_intact(st->guard_tail, V4_RET_BOUND, V4_GUARD_PATTERN_TAIL);
}
void v4_rstack_push(v4_rstack *st, v4_cell x)
{
st->ring[st->head] = st->r;
st->r = x;
st->head = (st->head + 1u) % V4_RET_RING;
}
v4_cell v4_rstack_pop(v4_rstack *st)
{
v4_cell x = st->r;
st->head = (st->head + V4_RET_RING - 1u) % V4_RET_RING;
st->r = st->ring[st->head];
return x;
}
v4_cell v4_rstack_peek(const v4_rstack *st)
{
return st->r;
}
+20
View File
@@ -0,0 +1,20 @@
/* testcode.c -- run a word on a node, for tests. See testcode.h. */
#include "v4/testcode.h"
long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
v4_cell entry, long max_words)
{
long words = 0;
v4_rstack_push(&n->rs, V4_TEST_HALT);
n->p = entry;
while (n->p != V4_TEST_HALT) {
if (words >= max_words) return -1;
/* A golden model must not index outside its own memory on a runaway
* P; node.h leaves out-of-range addressing undefined. */
if (n->p < 0 || (v4_ucell)n->p >= V4_NODE_WORDS) return -1;
(void)v4_exec_step_word(n, es, h);
words++;
}
return words;
}
+27
View File
@@ -0,0 +1,27 @@
/* umul.c -- reference unsigned multiply. See umul.h.
*
* Schoolbook multiplication on half-cells, in v4_ucell only. No wider type is
* used, so the same code is correct at both cell widths and builds on a host
* with no 128-bit integer (ARMv7 is one, and is a v4 host).
*/
#include "v4/umul.h"
#define HALF_BITS (V4_CELL_BITS / 2)
#define HALF_MASK ((((v4_ucell)1) << HALF_BITS) - 1u)
void v4_umul(v4_ucell u1, v4_ucell u2, v4_ucell *ulo, v4_ucell *uhi)
{
v4_ucell a0 = u1 & HALF_MASK, a1 = u1 >> HALF_BITS;
v4_ucell b0 = u2 & HALF_MASK, b1 = u2 >> HALF_BITS;
v4_ucell p00 = a0 * b0;
v4_ucell p10 = a1 * b0;
v4_ucell p01 = a0 * b1;
v4_ucell p11 = a1 * b1;
/* Cannot overflow: at most (2^h - 1) + (2^h - 1) + (2^h - 1)^2 = 2^2h - 1. */
v4_ucell mid = (p00 >> HALF_BITS) + (p10 & HALF_MASK) + p01;
if (ulo) *ulo = (mid << HALF_BITS) | (p00 & HALF_MASK);
if (uhi) *uhi = p11 + (p10 >> HALF_BITS) + (mid >> HALF_BITS);
}