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:
co-authored by
Claude Opus 5.5
parent
1f98034311
commit
067f317c47
+115
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user