Files
LithosAnanake/v4/src/exec.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

210 lines
6.2 KiB
C

/* 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;
}