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
+1
View File
@@ -170,6 +170,7 @@ definition below depends on one, it says so.
| **D-6** | Which heat structures exist in hardware: per-opcode counters only, or also per-call-target and word-to-word transition counters. | **Deferred** to step 2. The golden model implements per-opcode and per-call-target heat in the meantime. |
| **D-7** | `ROLL` semantics. | **Moot.** `ROLL` is retired under D-2. |
| **D-8** | Q48.16 signedness. | **Signed.** v3's unsigned comparisons and `Q.FROM-INT` clamping are retired. |
| **D-9** | Instruction word width on a 64-bit-cell host (ruled 2026-10-02). | **32 bits at every cell width.** Six 5-bit slots plus 2 spare bits, as in §1.2. On a 64-bit host the instruction word is the low 32 bits of the cell and the high half is ignored, so compiled code is identical at both widths. Only data and `@p` literals are a full cell wide. |
**Consequences of D-2 that every definition must respect.** The data stack holds 10 items and the
return stack 9, and every `call`, `FOR`, `DO` loop frame and `push` uses return-stack slots. Nesting
+2
View File
@@ -0,0 +1,2 @@
# Build output. Regenerated by `make test` / `make sanitize`.
build/
+105
View File
@@ -0,0 +1,105 @@
# v4/Makefile -- host golden model build.
#
# JUSTIFICATION.md section 5: the host node runs the same mesh-defined
# instruction set as the hardware, so the C99 model is the reference the
# hardware is compared against -- not a host-specific reimplementation.
# There is consequently nothing v4-specific in the architecture here; the
# build is plain hosted C99 and the ISA is defined in DECOMPOSITION.md.
#
# The two builds that matter are the two cell widths. v4/Makefile builds and
# runs the tests at both, because a width-dependent bug that only appears at
# 32 bits is exactly the failure mode the F18's 32-bit cells invite and
# exactly the one a 64-bit-only test run would miss.
#
# C99, warnings fatal. A warning here is a defect in a model whose only job
# is to be trusted.
CC ?= cc
CSTD := -std=c99
WARN := -Wall -Wextra -Wpedantic -Werror
OPT ?= -O2 -g
CFLAGS ?= $(CSTD) $(WARN) $(OPT)
# Anchor everything to this Makefile's directory so the build behaves the same
# whether it is invoked as `make -C v4 test` or `make -f v4/Makefile test`
# from the repository root. Without this the wildcards below silently expand
# to nothing from the root and the build "succeeds" having compiled no tests.
HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
SRCS := $(wildcard $(HERE)/src/*.c)
TESTS := $(wildcard $(HERE)/tests/*.c)
# Cell widths to build and verify. 32 is the F18/Zynq case, 64 the aarch64
# host case; see DECOMPOSITION.md D-5.
WIDTHS := 32 64
BINDIR := $(HERE)/build
.PHONY: all test sanitize clean $(addprefix test-,$(WIDTHS))
all: test
# A build that finds no tests must fail loudly, not report a vacuous pass. A
# loop over an empty list exits 0 having run nothing, which in CI is
# indistinguishable from a green run -- the worst possible failure mode for a
# test harness, and one this Makefile has already produced once.
ifeq ($(strip $(TESTS)),)
$(error No test sources under $(HERE)/tests -- refusing to report success)
endif
# One test binary per (width, test source) pair. The cell width is a
# preprocessor parameter rather than a runtime one, so the two widths are
# separate compilations -- which is the point: there is no single code path
# that could paper over a width assumption.
#
# `make test` builds optimised; `make sanitize` builds the same sources under
# AddressSanitizer and UndefinedBehaviorSanitizer at both widths and runs them.
# The optimized run catches logic errors, this one catches the out-of-bounds
# ring index and the width-dependent shift that a logic-only test cannot see.
define TEST_RULE
$(BINDIR)/$(1)-$(notdir $(2)): $(2) $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h)
@mkdir -p $(BINDIR)
$$(CC) $$(CFLAGS) -I$(HERE)/include -DV4_CELL_BITS=$(1) \
$(2) $$(SRCS) -o $$@
$(BINDIR)/san-$(1)-$(notdir $(2)): $(2) $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h)
@mkdir -p $(BINDIR)
$$(CC) $$(CSTD) $$(WARN) -O1 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined -I$(HERE)/include -DV4_CELL_BITS=$(1) \
$(2) $$(SRCS) -o $$@
endef
# One run target per (width, test) and one prerequisite line per width, so
# adding a test file cannot redefine a recipe and the run cannot skip the
# build. A shell loop over `$^` is avoided deliberately: `$^` is make syntax
# that does not survive the define/eval expansion, and a silently-empty loop
# list is exactly the vacuous-pass failure this file already made once.
define RUN_RULE
run-$(1)-$(notdir $(2)): $(BINDIR)/$(1)-$(notdir $(2))
@echo " [V4_CELL_BITS=$(1) $$@]"
@$(BINDIR)/$(1)-$(notdir $(2))
test-$(1): run-$(1)-$(notdir $(2))
endef
# The same sources and the same assertions, under ASan+UBSan.
define SAN_RULE
san-$(1)-$(notdir $(2)): $(BINDIR)/san-$(1)-$(notdir $(2))
@echo " [ASan+UBSan V4_CELL_BITS=$(1) $$@]"
@$(BINDIR)/san-$(1)-$(notdir $(2))
endef
$(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call TEST_RULE,$(w),$(t)))))
$(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call RUN_RULE,$(w),$(t)))))
$(foreach w,$(WIDTHS),$(foreach t,$(TESTS),$(eval $(call SAN_RULE,$(w),$(t)))))
sanitize: $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),san-$(w)-$(notdir $(t))))
@echo "all v4 tests passed under ASan+UBSan"
.PHONY: $(foreach w,$(WIDTHS),$(foreach t,$(TESTS),run-$(w)-$(notdir $(t)) san-$(w)-$(notdir $(t))))
test: $(addprefix test-,$(WIDTHS))
@echo "all v4 tests passed"
clean:
rm -rf $(BINDIR)
+9 -3
View File
@@ -4,6 +4,12 @@ StarForth v4: the 32-instruction F18-derived core. The design lives in
`docs/v4.0.0/JUSTIFICATION.md` (why) and `docs/v4.0.0/DECOMPOSITION.md`
(every v3 word mapped to a v4 fate).
There is no code yet. The first deliverable is the hosted C99 golden model
(`JUSTIFICATION.md` §10, step 1). It must pass POST and hold K≡1.0 with
32- and 64-bit cells on all three host ISAs.
The first deliverable is the hosted C99 golden model (`JUSTIFICATION.md` §10,
step 1). It must pass POST and hold K≡1.0 with 32- and 64-bit cells on all
three host ISAs.
What exists so far is the single node: registers, circular stacks, memory, all
32 opcodes, and per-opcode and per-call-target heat. `make -C v4 test` builds
and runs the tests at both cell widths; `make -C v4 sanitize` repeats them
under ASan and UBSan. There is no compiler capsule, no POST and no K
measurement yet.
+68
View File
@@ -0,0 +1,68 @@
/* asm.h -- slot packer for hand-assembled test programs.
*
* This is not the compiler capsule (JUSTIFICATION.md section 9). It exists so
* that tests can write a DECOMPOSITION.md definition opcode by opcode and have
* the slots, literals and branch targets laid out the way section 1.2 and
* section 2 require, instead of packing words by hand in every test.
*
* Layout rules it applies:
*
* - Opcodes fill slots 0..5 left to right; a full word is written out and a
* new one started. Unused slots are padded with `nop`.
* - A literal is `@p` in a slot plus its value in the word after the
* instruction word. Several literals in one word follow it in order.
* - `;` and `ex` close the word, since nothing after them executes.
* - A branch closes the word, since the bits to its right are its address.
* If the next free slot is 4 or 5 the word is closed first, because
* branches are legal in slots 0-3 only.
* - A branch target must lie in the same page as P at the time the branch
* executes, for the reach of the slot it landed in. Otherwise the
* assembler sets its error flag rather than emit a branch that goes
* somewhere else.
*/
#ifndef V4_ASM_H
#define V4_ASM_H
#include "v4/iword.h"
#include "v4/node.h"
typedef struct {
v4_node *n;
v4_cell here; /* address of the word being built */
v4_iword cur;
unsigned slot; /* next free slot, 0..V4_SLOT_COUNT */
v4_cell lit[V4_SLOT_COUNT]; /* literals owed after this word */
unsigned nlit;
int error;
} v4_asm;
/* A branch whose target is not known yet. */
typedef struct {
v4_cell addr; /* the word holding the branch */
unsigned slot;
v4_cell p; /* P when the branch executes */
} v4_asm_ref;
void v4_asm_begin(v4_asm *as, v4_node *n, v4_cell origin);
/* Any opcode that is neither a branch nor `@p`. */
void v4_asm_op(v4_asm *as, unsigned op);
/* `@p` and its value. */
void v4_asm_lit(v4_asm *as, v4_cell value);
/* jump, call, next, if, -if to a known address. */
void v4_asm_branch(v4_asm *as, unsigned op, v4_cell target);
/* The same with the target supplied later by v4_asm_resolve. */
v4_asm_ref v4_asm_branch_fwd(v4_asm *as, unsigned op);
void v4_asm_resolve(v4_asm *as, v4_asm_ref ref, v4_cell target);
/* Close the word being built and return the address of the next one. This is
* how a label is taken: branch targets are word addresses. */
v4_cell v4_asm_label(v4_asm *as);
/* 1 if nothing went wrong so far. Closes the word being built. */
int v4_asm_ok(v4_asm *as);
#endif /* V4_ASM_H */
+60
View File
@@ -0,0 +1,60 @@
/* cell.h -- v4 cell width as a build parameter.
*
* DECOMPOSITION.md D-5: the host node's cell width matches the host CPU
* (32 on a Zynq-7000 Cortex-A9, 64 on an aarch64 host). JUSTIFICATION.md
* section 5 makes this the second invariance axis: if K==1.0 holds across
* cell widths as well as across ISAs, the conservation law is shown not to
* depend on word size.
*
* The compiler capsule is written width-independent against these types.
* Nothing in v4 may assume a particular V4_CELL_BITS; if you find yourself
* wanting to, the code belongs in the host node, not the mesh.
*
* JUSTIFICATION.md section 5 also fixes the split that matters for the
* physics: heat and K arithmetic stay 64-bit on every host regardless of
* cell width, because they must not lose precision when the cell shrinks.
* That is why this header defines a separate v4_heat_t rather than reusing
* v4_cell.
*/
#ifndef V4_CELL_H
#define V4_CELL_H
#include <stdint.h>
#ifndef V4_CELL_BITS
#error "V4_CELL_BITS must be defined to 32 or 64 (e.g. -DV4_CELL_BITS=32)."
#endif
#if V4_CELL_BITS == 32
typedef int32_t v4_cell;
typedef uint32_t v4_ucell;
#elif V4_CELL_BITS == 64
typedef int64_t v4_cell;
typedef uint64_t v4_ucell;
#else
#error "V4_CELL_BITS must be 32 or 64."
#endif
/* Physics/heat arithmetic width. Fixed at 64 by JUSTIFICATION.md section 5:
* "Heat and K arithmetic remain 64-bit (int64_t in C99 on every host; double
* cells on a 32-bit node). Changing only the payload width keeps the
* experiment clean: any difference in K can be attributed to cell width and
* not to lost precision." The C side is therefore always 64-bit; the
* double-cell framing is the 32-bit node's problem, not this type's. */
typedef int64_t v4_heat_t;
typedef uint64_t v4_uheat_t;
/* MSB -- the cell-width top-bit constant.
*
* DECOMPOSITION.md section 5.5 (RSHIFT) and section 5.7 (D2/) both call for
* "MSB, the cell-width top-bit constant" but never define it, which leaves a
* bit-width-dependent constant to be invented mid-arithmetic. It is defined
* here once, as a value with exactly the cell's sign bit set and nothing
* else, so that the sign-test idioms in those two words are width-correct by
* construction at 32 and at 64. */
#define V4_MSB (((v4_ucell)1) << (V4_CELL_BITS - 1))
/* All-ones, the width-correct "true". FORTH-79 uses -1 for TRUE. */
#define V4_ALL_ONES ((v4_cell)-1)
#endif /* V4_CELL_H */
+56
View File
@@ -0,0 +1,56 @@
/* exec.h -- instruction execution for v4.
*
* DECOMPOSITION.md section 1.2: "Slots execute left to right." A branch
* (jump, call, next, if, -if) takes its target from all bits to the right of
* its slot and replaces the same low bits of P (page-relative). Branches are
* legal in slots 0-3 only. Nothing to the right of a branch executes, taken
* or not, because those bits are its address.
*
* Section 1.4: "Every retired instruction increments that opcode's heat counter
* and advances the anti-clock; every `call` increments the heat counter of its
* target. None of this costs an instruction."
*
* The node owns P, A, B, the stacks and memory; heat and the anti-clock are
* separate physics state (section 7 names them as registers).
*
* THREE POINTS THE SPEC LEAVES TO THE F18, and which this model takes from it:
*
* - `ex` ends the word, like `;`. P has been replaced, so the remaining
* slots belong to a word that is no longer the current one.
* - `unext` restarts the *already fetched* word at slot 0. It does not
* refetch and does not move P, so `@p` and `!p` inside a unext loop step
* through the words that follow the loop word. When R reaches 0 the loop
* ends and execution continues with the slot after `unext`.
* - `+*` shifts T arithmetically (sign bit kept). Section 1.3 says only
* "shift T:A right one bit"; D-3 says plain F18 semantics.
*
* A branch opcode in slot 4 or 5 is outside the spec. The model does not trap
* it: it applies the 7- or 2-bit target the layout formula gives.
*/
#ifndef V4_EXEC_H
#define V4_EXEC_H
#include "v4/heat.h"
#include "v4/iword.h"
#include "v4/node.h"
typedef struct {
v4_uheat_t anticlock;
} v4_exec_state;
void v4_exec_reset(v4_exec_state *es);
/* Execute one instruction word: fetch at P, advance P, run the slots left to
* right until the word ends. A unext loop runs to completion inside one call,
* so the call does not return while R is nonzero at a `unext`. Returns the
* number of instructions retired. */
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h);
/* Execute a single opcode. The branch opcodes read their target from `w` and
* `slot`; every other opcode ignores both. `unext` here only updates R; the
* restart is v4_exec_step_word's. A value above 31 does nothing and retires
* nothing. */
void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
unsigned op, v4_iword w, unsigned slot);
#endif /* V4_EXEC_H */
+92
View File
@@ -0,0 +1,92 @@
/* guard.h -- 5% safety boundaries at each end of every bounded list.
*
* The convention used throughout this project is that a bounded list carries a
* safety boundary at each end rather than trusting its own bounds. The kernel
* arena is the reference instance (kernel/src/vm/arena.c):
*
* #define SK_VM_GUARD_SIZE PMM_PAGE_SIZE
* #define SK_VM_GUARD_PATTERN_HEAD 0x5ac0ffeed0c0ffeeULL
* #define SK_VM_GUARD_PATTERN_TAIL 0x0bad0badf00df00dULL
*
* with a guard_fill / guard_check pair, and total size = payload + 2 * guard.
* Two properties of that instance are kept here deliberately:
*
* - HEAD and TAIL carry *different* patterns, so a check that fails can say
* which end was overrun. A single pattern tells you that something ran
* off the end and nothing about where.
* - the boundary is placed immediately adjacent to the payload, because a
* boundary separated from what it protects catches nothing.
*
* This header supplies the same facility at a size that scales with the list
* (5% at each end) rather than a fixed page, and reduces it to two helpers so
* that no list in v4 has to hand-roll the arithmetic.
*
* WHY THIS IS LOAD-BEARING HERE, NOT DECORATIVE. AddressSanitizer already
* catches a genuine out-of-struct access, so a guard band cannot be justified
* as "catching overflow" -- ASan does that. What ASan does *not* catch is an
* index that is wrong but still lands inside the same object, which is exactly
* the failure mode of a circular buffer driven by unsigned index arithmetic:
*
* head = (head + V4_DATA_RING - 1u) % V4_DATA_RING; correct
* head = (head + V4_DATA_RING - 1u) & (V4_DATA_RING - 1u); correct today
*
* The second line is identical today *only because 8 is a power of two*. It
* is the sort of "simplification" that gets made, it is right for the current
* depths, and it silently breaks the first time a ring size stops being a power
* of two. Both forms stay in range, so ASan stays quiet. A guard band with a
* 5% margin either side is what turns "the tests happened to cover it" into a
* check that fires. The tests in tests/test_stack.c exercise that directly:
* they index past each end on purpose and require the check to catch it.
*
* The boundary is sized with ceiling division and a floor of one element, so a
* short list still gets a real boundary. A 5% margin on a 3-element list
* rounds to 0, and a zero-width guard is not a guard -- it is a comment.
*/
#ifndef V4_GUARD_H
#define V4_GUARD_H
#include "v4/cell.h"
/* Fraction of the payload reserved as a boundary at each end. */
#define V4_GUARD_PCT 5u
/* Boundary width, in elements, for a payload of n elements: 5% rounded up,
* never less than one. Written as a macro so that a struct can size its own
* inline boundary at compile time -- the boundary has to be storage the object
* owns, not something a helper allocates later, because a mesh node has no
* heap to allocate it from.
*
* n = 8 -> 1 (5% = 0.4, rounds up)
* n = 32 -> 2 (5% = 1.6, rounds up)
* n = 100 -> 5
* n = 1000 -> 50
* n = 3 -> 1 (5% = 0.15, rounds up to 1 -- not 0)
*/
#define V4_GUARD_ELEMS(n) \
(((n) * V4_GUARD_PCT + 99u) / 100u)
/* The kernel arena's patterns, carried over so that a hex dump from a mesh
* node and one from the kernel can be read with the same key. Casting to
* v4_ucell truncates cleanly at 32-bit cells: the arena's own constants are
* 64-bit, and their low halves are as distinct as the full words --
* 0xd0c0ffee against 0xf00df00d -- so "which end" stays answerable at both
* widths. */
#define V4_GUARD_PATTERN_HEAD ((v4_ucell)0x5ac0ffeed0c0ffeeULL)
#define V4_GUARD_PATTERN_TAIL ((v4_ucell)0x0bad0badf00df00dULL)
/* If these ever compare equal the boundary has stopped saying which end was
* overrun, which is half its purpose. C99 has no _Static_assert. */
typedef char v4_guard_patterns_are_distinct
[(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL) ? 1 : -1];
/* Write one boundary band. `band` is the start of the band, `n` its width in
* elements. */
void v4_guard_fill(v4_cell *band, unsigned n, v4_ucell pattern);
/* 1 if the whole band still holds `pattern`, else 0. Checks every element, not
* just the ends: the band is 5% wide precisely so there is something to check
* in it, and a single word is what the kernel arena can afford at page
* granularity. */
int v4_guard_intact(const v4_cell *band, unsigned n, v4_ucell pattern);
#endif /* V4_GUARD_H */
+79
View File
@@ -0,0 +1,79 @@
/* heat.h -- heat and anti-clock, not instructions themselves (1.4).
*
* DECOMPOSITION.md section 1.4 states: "Execution itself drives the physics.
* Every retired instruction increments that opcode's heat counter and advances
* the anti-clock; every `call` increments the heat counter of its target. None
* of this costs an instruction."
*
* D-6 is deferred to step 2, but the note immediately adds: "The golden model
* implements per-opcode and per-call-target heat in the meantime." The
* counters here are therefore a two-dimensional heat structure (op × target)
* on a per-call basis, with counts stored in 64-bit (because heat is
* accumulated and must not roll over at 32-bit). The "per-call-target"
* counter is indexed by the address of the called target -- on a word-addressed
* node, that is a cell value equal to the address `a` passed to `call a`.
*
* The anti-clock is a virtual tick driven by execution -- one tick per retired
* instruction -- and the rest of §7 refers to it via `ANTICLOCK`. It is not a
* wall-clock time, it is a function of the execution stream, as specified.
*
* The MM names in §7: `HEAT-OP[0..31]` (R), `HEAT-CALL[...]` (R/W) with a
* freeze bit (D-6). D-4 (memory map) is deferred, so the model does not
* materialize those MM addresses yet. This header defines the backing storage
* that a later MM layer will expose; it keeps the counts separate from the node
* itself because heat is physics data, not the instruction word stream, and it
* needs to be accessible to assertions such as K conservation tests later.
*
* Counts are 64-bit. Even for a short run a call-heavy word can heat up many
* times, and the golden model must run POST and remain monotonic and
* comparable. They are stored as `v4_uheat_t` (which is uint64_t by cell-width
* parameterisation) rather than cell-sized so that a 32-bit node's heat is not
* width-truncated in the model. (JUSTIFICATION.md §5 keeps heat arithmetic
* 64-bit regardless of cell width.)
*/
#ifndef V4_HEAT_H
#define V4_HEAT_H
#include "v4/cell.h"
#include "v4/opcode.h"
#define V4_HEAT_MAX_CALL_TARGETS 4096u
typedef struct {
v4_uheat_t op[V4_OPCODE_COUNT]; /* HEAT-OP[0..31] */
/* Per-call-target heat. Indexed by target address. The table is fixed-size
* for the golden model; the actual hardware table size is deferred with D-6,
* but "in the meantime" the model must have a per-call-target structure.
* 4096 entries is small (32KB if using 64-bit per entry), enough to cover a
* typical test program's call targets without needing to allocate on-the-fly
* -- and since the node has boundaries, over-indexing is caught. */
v4_uheat_t call[V4_HEAT_MAX_CALL_TARGETS];
v4_ucell call_freeze_mask[V4_HEAT_MAX_CALL_TARGETS / (sizeof(v4_ucell) * 8u)];
/* freeze bits: one per call-target entry (D-6). Stored as a bitmask. */
} v4_heat;
void v4_heat_reset(v4_heat *h);
/* Every retired instruction: +1 to the opcode's heat and +1 to ANTICLOCK. */
void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock);
/* Every call: +1 to the call-target's heat. The target is the address passed
* to call (word address). If the target is outside the table range, the model
* must not silently wrap -- a boundary-style check is the conservative choice
* here, because the table size is a model parameter. */
void v4_heat_on_call(v4_heat *h, v4_cell target);
/* Read/write helpers for per-call-target heat (R/W). Freeze bits are set per
* target when D-6's freeze bit is asserted; writes to a frozen target should be
* ignored in a real implementation, but the MM layer is not defined yet. The
* golden model stores freeze bits and the accessor can enforce them later. */
v4_uheat_t v4_heat_call_get(const v4_heat *h, v4_cell target);
int v4_heat_call_set(v4_heat *h, v4_cell target, v4_uheat_t val);
/* Freeze control (D-6). */
void v4_heat_call_freeze(v4_heat *h, v4_cell target);
void v4_heat_call_unfreeze(v4_heat *h, v4_cell target);
int v4_heat_call_is_frozen(const v4_heat *h, v4_cell target);
#endif /* V4_HEAT_H */
+101
View File
@@ -0,0 +1,101 @@
/* iword.h -- the 32-bit instruction word and its decode.
*
* DECOMPOSITION.md section 1.2:
*
* 31 27 26 22 21 17 16 12 11 7 6 2 1 0
* +--------+--------+--------+--------+--------+--------+----+
* | slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | xx |
* +--------+--------+--------+--------+--------+--------+----+
*
* "Slots execute left to right. A branch (jump, call, next, if, -if) takes
* its target from all bits to the right of its slot, and that target replaces
* the same low bits of P (page-relative, as in the F18). Branches are
* therefore legal in slots 0-3 only", with reach 27 / 22 / 17 / 12 bits.
*
* That reach column is not a separate rule, it falls out of the layout: slot k
* sits at bits [31-5k : 27-5k], so the bits to its right are [26-5k : 0],
* which is 27-5k bits -- 27, 22, 17, 12 for slots 0..3. This header derives
* the masks from that one formula rather than hard-coding four of them, so the
* decode cannot drift from the reach table.
*
* THE SPARE BITS ARE ADDRESS BITS. "Spare" means only that they are never
* *executed*: they are below every slot, so "all bits to the right of its
* slot" includes them, and they are the low bits of the address of a branch in
* any slot. They are inert for opcode decode and live in a branch target.
* This is easy to assume otherwise and get wrong -- an earlier revision of
* this header subtracted them from the branch width, which silently cost two
* address bits off every branch and was caught by tests/test_iword.c. They
* exist because 6*5 = 30 does not fill a 32-bit word, and on the F18 those low
* bits are simply the bottom of the address space.
*
* The consequence for the compiler is worth stating: a non-branch slot's low
* bits are free to carry a compressed argument, but a branch slot's are not,
* because they are address.
*
* NOTE ON WIDTH. The word format is 32 bits regardless of V4_CELL_BITS: the
* diagram is 32 bits wide and 6*5+2 = 32 exactly. D-5 makes the *cell* width
* a build parameter, but the instruction word is not a payload and does not
* follow the cell. At 32-bit cells a word is a cell. At 64-bit cells it is
* the low 32 bits of the cell and the high half is ignored (D-9), so compiled
* code is identical at both widths. The decode below operates on a distinct
* 32-bit type; the fetch in exec.c does the narrowing.
*/
#ifndef V4_IWORD_H
#define V4_IWORD_H
#include <stdint.h>
#include "v4/cell.h"
#include "v4/opcode.h"
/* The instruction word is 32 bits at every cell width. */
typedef uint32_t v4_iword;
#define V4_SLOT_COUNT 6u /* six 5-bit slots, DECOMPOSITION.md 1.1 */
#define V4_SPARE_BITS 2u /* "plus 2 spare bits" */
/* C99 has no _Static_assert, so the "6*5+2 == 32" identity is enforced with
* the negative-array-size idiom. If the slot count, the spare-bit count or the
* word width ever stop adding up to 32, this fails to compile. */
typedef char v4_iword_layout_is_exact
[((V4_SLOT_COUNT * 5u + V4_SPARE_BITS) == 32u) ? 1 : -1];
/* Bit range of slot k: high bit 31-5k, low bit 27-5k. */
#define V4_SLOT_HIGH_BIT(k) (31u - 5u * (k))
#define V4_SLOT_LOW_BIT(k) (27u - 5u * (k))
/* Number of address bits a branch in slot k carries: all bits to the right of
* the slot. Slot k's low bit is 27-5k, so the bits below it are 0..(26-5k),
* which is 27-5k bits -- equal to the slot's own low bit. This is the single
* source of the reach column, and it is why the spare bits are *not* subtracted
* here: see the note on the spare bits below. */
#define V4_BRANCH_ADDR_BITS(k) (V4_SLOT_LOW_BIT(k))
/* Mask of the low address bits a branch in slot k replaces in P. */
uint32_t v4_iword_slot_mask(unsigned slot);
/* The 5-bit opcode in slot k, 0..31. */
unsigned v4_iword_op(v4_iword w, unsigned slot);
/* The 2 spare low bits. Never executed. */
unsigned v4_iword_spare(v4_iword w);
/* The branch target carried by a branch in slot k: all bits to the right of
* the slot, zero-extended. Meaningful only for the five branch opcodes. */
v4_iword v4_iword_target(v4_iword w, unsigned slot);
/* Assemble a word from six 5-bit opcodes and the 2 spare bits. Opcode values
* above 31 or spare bits above 3 are rejected, because a golden model must not
* paper over an out-of-range encoding. */
v4_iword v4_iword_assemble(const unsigned op[V4_SLOT_COUNT], unsigned spare);
/* Apply a branch: replace the low V4_BRANCH_ADDR_BITS(slot) bits of P with the
* target, preserving the bits above (page-relative, F18-style). */
v4_cell v4_iword_branch(v4_cell p, v4_iword w, unsigned slot);
/* Branches are legal in slots 0-3 only (section 1.2). Slots 4 and 5 have 7 and
* 2 address bits respectively, which is not a useful reach and is not
* sanctioned by the table. */
int v4_iword_branch_legal(unsigned slot);
#endif /* V4_IWORD_H */
+114
View File
@@ -0,0 +1,114 @@
/* node.h -- the v4 node: registers, stacks, and word-addressed memory.
*
* DECOMPOSITION.md section 1.1:
*
* | Cell | 32 bits (mesh node). Host node's width is a build
* | parameter; see D-5. |
* | Addressing | Word-addressed (D-1). |
* | Registers | T (top of data stack), S (second), R (top of return
* | stack), P (program counter), A and B (address
* | registers). |
* | Stacks | F18 circular hardware stacks, not visible to code
* | (D-2) ... data stack 10 deep, return stack 9 deep. |
* | Instruction | Six 5-bit slots, plus 2 spare bits. |
*
* T, S and R are not fields here: they live inside v4_dstack and v4_rstack,
* because on the F18 they are the topmost slots of the circular stacks rather
* than registers that happen to shadow them, and the golden model has to model
* the mechanism and not just the values. P, A and B are genuinely separate
* registers and live here.
*
* MEMORY SIZE IS A BUILD PARAMETER, not a constant of the design.
* JUSTIFICATION.md section 10 names cell width, node count and node memory as
* the three parameters of the golden model, and no word count is fixed anywhere
* in DECOMPOSITION.md -- D-4, which is where a node memory map would be pinned
* down, is deferred to step 2. The default here is a round number that suits
* a host; the datapoint worth knowing is JUSTIFICATION.md section 4, that a
* GA144 node had 64 words of RAM and 64 of ROM. v4 keeps the same split --
* dictionary and interpreter live in the compiler capsule on the host and are
* streamed to nodes, so node memory is not sized by capsule size.
*/
#ifndef V4_NODE_H
#define V4_NODE_H
#include "v4/cell.h"
#include "v4/guard.h"
#include "v4/stack.h"
/* Words of node memory. Overridable at build time. */
#ifndef V4_NODE_WORDS
#define V4_NODE_WORDS 1024u
#endif
/* 5% safety boundary at each end of node memory, per guard.h. Note the
* arithmetic: at 1024 words that is 52 words either side, 5.2% of memory
* spent on a boundary. That is the cost of the convention on a resource that
* is scarce by construction, and it is worth naming rather than discovering
* later -- on a real mesh node the memory is 32-bit words on silicon with no
* second chance. A deployment that cannot afford it sets V4_GUARD_PCT to 0 at
* build time, which is why the percentage is a macro and not baked in. */
#define V4_MEM_BOUND V4_GUARD_ELEMS(V4_NODE_WORDS)
typedef struct {
v4_cell p; /* P -- program counter */
v4_cell a; /* A -- address register */
v4_cell b; /* B -- address register */
v4_dstack ds; /* holds T and S */
v4_rstack rs; /* holds R */
v4_cell mem_guard_head[V4_MEM_BOUND];
v4_cell mem[V4_NODE_WORDS];
v4_cell mem_guard_tail[V4_MEM_BOUND];
} v4_node;
/* Zero P, A and B, empty the stacks, and clear memory. Installs every
* boundary (memory and both stacks) and leaves the node in a defined state. */
void v4_node_reset(v4_node *n);
/* 1 if every boundary in the node is intact -- memory and both stacks -- else
* 0. This is the single call a caller needs to detect that an index went
* wrong somewhere; see guard.h for what it catches that AddressSanitizer
* cannot. */
int v4_node_guards_intact(const v4_node *n);
/* Read and write one word. D-1: word-addressed, so `addr` counts words and
* CELLS is a no-op. `addr` is a v4_cell because A, B and P are all v4_cells
* and a narrower parameter would put a conversion between the register and the
* access on every one of the eight memory opcodes.
*
* PRECONDITION: 0 <= addr < V4_NODE_WORDS.
*
* Out-of-range addressing is not defined by DECOMPOSITION.md. D-1 fixes word
* addressing and says nothing about an address outside the node's memory; a
* F18 has no such case because its address space is the memory. Whether a
* conforming model should wrap, saturate, or fault is a real open question and
* is raised as a gap rather than decided here, because each answer changes the
* ISA's observable behaviour and inventing one silently would bake a guess into
* every later test.
*
* WHAT THE BOUNDARY HERE DOES AND DOES NOT COVER, since it is easy to
* overclaim and the distinction decides whether out-of-range addresses are
* actually protected:
*
* - It DOES cover a linear index error. An access to mem[-1] or
* mem[V4_NODE_WORDS] lands in the 5% band, because the band is immediately
* adjacent (tests/test_node.c asserts the adjacency with offsetof rather
* than assuming it). That access is inside the struct, so AddressSanitizer
* stays silent about it and v4_node_guards_intact() is what reports it.
*
* - It does NOT cover an out-of-range *word address*. A v4_cell address of
* -1 casts to unsigned 0xFFFFFFFF, which is 16 GB past mem at 32-bit cells
* -- far outside the struct and outside the band. So a P, A or B that runs
* off the end of memory is caught by neither the band nor, in a production
* build without sanitizers, by anything else here. It is a precondition
* violation and the range check that would catch it is the open question
* above, not the band.
*
* The band is worth its 10.2% for the first case regardless. The second case
* is a hole, and it is closed by ruling on out-of-range addressing, not by
* widening the band. */
v4_cell v4_node_load(const v4_node *n, v4_cell addr);
void v4_node_store(v4_node *n, v4_cell addr, v4_cell value);
#endif /* V4_NODE_H */
+64
View File
@@ -0,0 +1,64 @@
/* opcode.h -- the 32 core opcodes.
*
* DECOMPOSITION.md section 1.3. "Opcode numbering follows the F18. Two names
* differ from Moore's: F18 `-` is renamed `inv` and F18 `or` (which is
* exclusive-or) is renamed `xor`, so instruction names never collide with
* FORTH-79 word names."
*
* Those two renames are load-bearing, not cosmetic: FORTH-79 has words named
* `-` and `OR`, and the DECOMPOSITION table would otherwise have two distinct
* meanings per name. The renames are therefore asserted against the FORTH
* name set in tests/test_iword.c, so a future edit that reintroduces a
* collision fails a test rather than silently producing an ambiguous table.
*
* Branch opcodes are marked because the instruction-word format depends on
* them: only these five take a target out of the low bits of their slot, and
* only they are legal outside the low four slots (section 1.2).
*/
#ifndef V4_OPCODE_H
#define V4_OPCODE_H
typedef enum {
V4_OP_SEMI = 0x00, /* ; return */
V4_OP_EX = 0x01, /* ex swap P and R */
V4_OP_JUMP = 0x02, /* jump a */
V4_OP_CALL = 0x03, /* call a */
V4_OP_UNEXT = 0x04, /* unext */
V4_OP_NEXT = 0x05, /* next a */
V4_OP_IF = 0x06, /* if a */
V4_OP_MINUS_IF = 0x07, /* -if a */
V4_OP_FETCH_P = 0x08, /* @p literal */
V4_OP_FETCH_INC= 0x09, /* @+ */
V4_OP_FETCH_B = 0x0A, /* @b */
V4_OP_FETCH_A = 0x0B, /* @ */
V4_OP_STORE_P = 0x0C, /* !p */
V4_OP_STORE_INC= 0x0D, /* !+ */
V4_OP_STORE_B = 0x0E, /* !b */
V4_OP_STORE_A = 0x0F, /* ! */
V4_OP_MUL_STEP = 0x10, /* +* multiply step, D-3 */
V4_OP_TWO_STAR = 0x11, /* 2* */
V4_OP_TWO_SLASH= 0x12, /* 2/ */
V4_OP_INV = 0x13, /* inv (F18 `-`) */
V4_OP_ADD = 0x14, /* + */
V4_OP_AND = 0x15, /* and */
V4_OP_XOR = 0x16, /* xor (F18 `or`) */
V4_OP_DROP = 0x17, /* drop */
V4_OP_DUP = 0x18, /* dup */
V4_OP_RPOP = 0x19, /* pop R to data stack */
V4_OP_OVER = 0x1A, /* over */
V4_OP_PUSH_A = 0x1B, /* a */
V4_OP_NOP = 0x1C, /* nop */
V4_OP_PUSH = 0x1D, /* push T to return stack */
V4_OP_BANG_B = 0x1E, /* b! B <- T */
V4_OP_BANG_A = 0x1F, /* a! A <- T */
V4_OPCODE_COUNT = 0x20
} v4_opcode;
/* True for the five opcodes that take a branch target from the low bits of
* their slot: jump, call, next, if, -if. Everything else ignores the low
* bits of its slot, which is what makes a non-branch slot's low bits
* free for the compiler to use as a compressed argument. */
int v4_op_is_branch(unsigned op);
#endif /* V4_OPCODE_H */
+89
View File
@@ -0,0 +1,89 @@
/* stack.h -- F18 circular hardware stacks (DECOMPOSITION.md D-2).
*
* D-2 rules these explicitly: "F18 circular stacks, hidden. Data stack 10
* deep (T, S + 8 circular), return stack 9 deep (R + 8 circular), exactly as
* the F18. No stack pointer is visible to code, so DEPTH, PICK, ROLL, .S,
* SP@ and SP! are retired everywhere, host node included."
*
* Two consequences that the ISA and the capsule code must both respect:
*
* 1. There is no overflow or underflow detection, and there never will be.
* Pushing past the bottom silently overwrites the oldest entry. This is
* hardware behaviour, reproduced faithfully here rather than defended
* against: a golden model that trapped on overflow would diverge from
* the thing it is a model of, which is the whole point of the exercise.
*
* 2. Nesting is therefore shallow (10 and 9), and exceeding either depth
* corrupts silently rather than faulting. DECOMPOSITION.md section 3
* calls this out as the constraint that "every CAP definition in section
* 4 and 5 must be re-checked against ... before it is accepted as a POST
* target; the hand traces so far assumed unbounded stacks."
*
* The T/S + 8-ring decomposition is the hardware shape, not an optimisation:
* T and S are architectural registers (section 1.1 lists them), and the ring
* is the circular part behind them. It is provably equivalent to a flat
* 10-deep circular buffer, which is what tests/test_stack.c checks it
* against -- see the equivalence argument in stack.c.
*/
#ifndef V4_STACK_H
#define V4_STACK_H
#include "v4/cell.h"
#include "v4/guard.h"
/* D-2: data stack is T, S + 8 circular = 10 deep. */
#define V4_DATA_RING 8
#define V4_DATA_DEPTH (V4_DATA_RING + 2)
/* D-2: return stack is R + 8 circular = 9 deep. */
#define V4_RET_RING 8
#define V4_RET_DEPTH (V4_RET_RING + 1)
/* 5% safety boundary at each end of each ring, per guard.h. The bands sit
* immediately either side of `ring` -- a boundary separated from the payload
* by another field catches nothing -- and are storage the struct owns, because
* a mesh node has no heap. Widths are 1 element each at the current ring
* sizes; V4_GUARD_ELEMS scales them if a ring size ever changes. */
#define V4_DATA_BOUND V4_GUARD_ELEMS(V4_DATA_RING)
#define V4_RET_BOUND V4_GUARD_ELEMS(V4_RET_RING)
typedef struct {
v4_cell t; /* T -- top of data stack (a register) */
v4_cell s; /* S -- second (a register) */
unsigned head;
v4_cell guard_head[V4_DATA_BOUND];
v4_cell ring[V4_DATA_RING];
v4_cell guard_tail[V4_DATA_BOUND];
} v4_dstack;
typedef struct {
v4_cell r; /* R -- top of return stack (a register) */
unsigned head;
v4_cell guard_head[V4_RET_BOUND];
v4_cell ring[V4_RET_RING];
v4_cell guard_tail[V4_RET_BOUND];
} v4_rstack;
void v4_dstack_reset(v4_dstack *st);
void v4_dstack_push(v4_dstack *st, v4_cell x);
v4_cell v4_dstack_pop(v4_dstack *st);
/* Read the top without disturbing the stack. T is a register on hardware, so
* this is a plain read and needs no ring access. */
v4_cell v4_dstack_peek(const v4_dstack *st);
/* Read the second element without disturbing the stack. */
v4_cell v4_dstack_peek2(const v4_dstack *st);
void v4_rstack_reset(v4_rstack *st);
void v4_rstack_push(v4_rstack *st, v4_cell x);
v4_cell v4_rstack_pop(v4_rstack *st);
v4_cell v4_rstack_peek(const v4_rstack *st);
/* 1 if both boundaries of the data stack are intact, else 0. See guard.h for
* what this catches that AddressSanitizer does not. */
int v4_dstack_guards_intact(const v4_dstack *st);
int v4_rstack_guards_intact(const v4_rstack *st);
#endif /* V4_STACK_H */
+19
View File
@@ -0,0 +1,19 @@
/* testcode.h -- run a word on a node, for tests. */
#ifndef V4_TESTCODE_H
#define V4_TESTCODE_H
#include "v4/exec.h"
/* Where a word called by v4_test_call returns to. Nothing is executed there:
* arriving is the stop condition. */
#define V4_TEST_HALT ((v4_cell)(V4_NODE_WORDS - 1u))
/* Call the word at `entry` as a caller would: push a return address, set P,
* and step instruction words until the word returns. The stacks and A, B are
* left as the caller set them, so arguments go on the data stack first.
* Returns the number of instruction words stepped, or -1 if the word had not
* returned after `max_words`. */
long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
v4_cell entry, long max_words);
#endif /* V4_TESTCODE_H */
+31
View File
@@ -0,0 +1,31 @@
/* umul.h -- unsigned multiply fixing UM* under D-3 constraints.
*
* DECOMPOSITION.md line 205-207: "NEEDS REVISION: written for the carry-keeping
* +* that D-3 rejected. With plain F18 +* the carry out of T is lost, so this
* is only correct when the partial sums never overflow T (operands below 2^31).
* A full-range UM* needs a correction step; to be rewritten and proven on the
* golden model."
*
* D-3 states: "Plain F18 semantics. The carry out of T is not kept."
* The +* defined in the F18 shifts T:A right; the add is T <- T+S when bit 0
* of A was set, but any carry out of that addition is discarded (not shifted
* into the high part). That means UM* as written only works for inputs whose
* partial products don't generate a carry beyond T's width during the loop.
*
* We need a correct full-range unsigned multiply (u1*u2 -> ulo, uhi) that is
* correct for all cell-width values of u1,u2, expressed in terms of the v4
* primitives or at least specified precisely for the golden model. Since
* we're building the golden model in C first, the canonical implementation is
* the width-correct C multiplication split into high/low halves at V4_CELL_BITS.
* This header provides both: a reference C implementation (for verification)
* and a documented form that matches D-3's +* constraint.
*/
#ifndef V4_UMUL_H
#define V4_UMUL_H
#include "v4/cell.h"
/* Unsigned multiply: (u1, u2) -> (ulo, uhi). Full range, width-correct. */
void v4_umul(v4_ucell u1, v4_ucell u2, v4_ucell *ulo, v4_ucell *uhi);
#endif /* V4_UMUL_H */
+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);
}
+110
View File
@@ -0,0 +1,110 @@
/* test_asm.c -- the slot packer lays words out as asm.h says it does. */
#include "v4/asm.h"
#include "v4/testcode.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_asm as;
static void fresh(v4_cell origin)
{
v4_node_reset(&n);
v4_exec_reset(&es);
v4_heat_reset(&h);
v4_asm_begin(&as, &n, origin);
}
static v4_iword word_at(v4_cell addr) { return (v4_iword)(v4_ucell)n.mem[addr]; }
int main(void)
{
v4_asm_ref ref;
v4_cell loop, other;
printf("v4 asm tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
/* Six opcodes fill a word; the seventh starts the next, padded with nop. */
fresh(0);
for (unsigned k = 0; k < 7; k++) v4_asm_op(&as, V4_OP_DUP);
CHECK(v4_asm_label(&as) == 2, "seven opcodes take two words");
for (unsigned k = 0; k < 6; k++) CHECK(v4_iword_op(word_at(0), k) == V4_OP_DUP, "word 0 slot %u", k);
CHECK(v4_iword_op(word_at(1), 0) == V4_OP_DUP, "word 1 slot 0");
for (unsigned k = 1; k < 6; k++) CHECK(v4_iword_op(word_at(1), k) == V4_OP_NOP, "word 1 pad %u", k);
/* `;` closes the word. */
fresh(0);
v4_asm_op(&as, V4_OP_DUP); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_label(&as) == 1, "; closes the word");
/* Literals follow the instruction word, in order, and the code runs. */
fresh(0);
v4_asm_lit(&as, 30); v4_asm_lit(&as, 12); v4_asm_op(&as, V4_OP_ADD); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "literal program assembles");
CHECK(n.mem[1] == 30 && n.mem[2] == 12 && v4_asm_label(&as) == 3, "literal layout");
CHECK(v4_test_call(&n, &es, &h, 0, 10) == 1 && n.ds.t == 42, "30 12 + ;");
/* A branch takes the next free slot when that slot is legal. */
fresh(0);
v4_asm_op(&as, V4_OP_DUP); v4_asm_op(&as, V4_OP_DROP);
v4_asm_branch(&as, V4_OP_JUMP, 0x155);
CHECK(v4_iword_op(word_at(0), 2) == V4_OP_JUMP, "branch in slot 2");
CHECK(v4_iword_target(word_at(0), 2) == 0x155, "branch target in slot 2");
CHECK(v4_asm_label(&as) == 1, "a branch closes the word");
/* With slots 0-3 used, the branch moves to slot 0 of a new word. */
fresh(0);
for (unsigned k = 0; k < 4; k++) v4_asm_op(&as, V4_OP_DUP);
v4_asm_branch(&as, V4_OP_JUMP, 0x155);
CHECK(v4_iword_op(word_at(0), 4) == V4_OP_NOP && v4_iword_op(word_at(0), 5) == V4_OP_NOP,
"word before the branch is padded");
CHECK(v4_iword_op(word_at(1), 0) == V4_OP_JUMP && v4_iword_target(word_at(1), 0) == 0x155,
"branch moved to slot 0");
/* Forward reference, section 2's IF ... ELSE ... THEN with the flag left
* on the stack by `if`: if L1 drop 111 ; L1: drop 222 ; */
fresh(0);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
v4_asm_op(&as, V4_OP_DROP); v4_asm_lit(&as, 111); v4_asm_op(&as, V4_OP_SEMI);
other = v4_asm_label(&as);
v4_asm_resolve(&as, ref, other);
v4_asm_op(&as, V4_OP_DROP); v4_asm_lit(&as, 222); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "forward branch assembles");
v4_dstack_push(&n.ds, 5);
CHECK(v4_test_call(&n, &es, &h, 0, 10) > 0 && n.ds.t == 111, "flag nonzero takes the first arm");
v4_dstack_reset(&n.ds); v4_rstack_reset(&n.rs);
v4_dstack_push(&n.ds, 0);
CHECK(v4_test_call(&n, &es, &h, 0, 10) > 0 && n.ds.t == 222, "flag zero takes the second arm");
/* Backward reference: 4 FOR 1 + NEXT runs the body n+1 times. */
fresh(0);
v4_asm_lit(&as, 4); v4_asm_op(&as, V4_OP_PUSH);
loop = v4_asm_label(&as);
v4_asm_lit(&as, 1); v4_asm_op(&as, V4_OP_ADD);
v4_asm_branch(&as, V4_OP_NEXT, loop);
v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "loop assembles");
v4_dstack_push(&n.ds, 0);
CHECK(v4_test_call(&n, &es, &h, 0, 50) > 0 && n.ds.t == 5, "4 FOR 1 + NEXT");
/* Errors are reported, not encoded. */
fresh(0); v4_asm_op(&as, V4_OP_JUMP);
CHECK(!v4_asm_ok(&as), "a branch through v4_asm_op is refused");
fresh(0); v4_asm_op(&as, V4_OPCODE_COUNT);
CHECK(!v4_asm_ok(&as), "an out-of-range opcode is refused");
fresh(0);
for (unsigned k = 0; k < 3; k++) v4_asm_op(&as, V4_OP_DUP);
v4_asm_branch(&as, V4_OP_JUMP, 5000); /* slot 3 reaches 4096 words */
CHECK(!v4_asm_ok(&as), "a target outside the page is refused");
fresh((v4_cell)(V4_NODE_WORDS - 1u));
v4_asm_lit(&as, 1); v4_asm_op(&as, V4_OP_SEMI);
CHECK(!v4_asm_ok(&as), "running off the end of memory is refused");
CHECK(v4_node_guards_intact(&n), "and does not touch the guard band");
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
/* test_foundation.c -- the first DECOMPOSITION.md definitions, executed.
*
* Section 4 says of its colon definitions that each "has been traced by hand,
* but none has been executed". This file assembles the helpers, the sign and
* zero tests, U< and UM* exactly as written there (plus 2DUP and - from
* section 5, which U< needs) and runs them on the golden model against the C
* operation each one stands for.
*
* Every call is made with a canary under the arguments, and the canary must
* still be directly under the results afterwards: a definition that leaves
* the right answer but an unbalanced stack is wrong.
*/
#include "v4/asm.h"
#include "v4/testcode.h"
#include "v4/umul.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
#define CANARY ((v4_cell)0x0C0FFEE5)
#define MAXU ((v4_ucell)~(v4_ucell)0)
#define FLAG(c) ((c) ? V4_ALL_ONES : (v4_cell)0)
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_asm as;
static v4_cell w_nip, w_swap, w_or, w_negate, w_rot, w_zless, w_zequal,
w_2dup, w_minus, w_uless, w_umstar;
#define O(name) v4_asm_op(&as, V4_OP_##name)
#define LIT(v) v4_asm_lit(&as, (v4_cell)(v))
#define CALL(w) v4_asm_branch(&as, V4_OP_CALL, (w))
static void build(void)
{
v4_asm_ref ref;
v4_node_reset(&n);
v4_asm_begin(&as, &n, 16);
/* : NIP push drop pop ; */
w_nip = v4_asm_label(&as);
O(PUSH); O(DROP); O(RPOP); O(SEMI);
/* : SWAP over push push drop pop pop ; */
w_swap = v4_asm_label(&as);
O(OVER); O(PUSH); O(PUSH); O(DROP); O(RPOP); O(RPOP); O(SEMI);
/* : OR over inv and xor ; */
w_or = v4_asm_label(&as);
O(OVER); O(INV); O(AND); O(XOR); O(SEMI);
/* : NEGATE inv 1 + ; */
w_negate = v4_asm_label(&as);
O(INV); LIT(1); O(ADD); O(SEMI);
/* : ROT push SWAP pop SWAP ; */
w_rot = v4_asm_label(&as);
O(PUSH); CALL(w_swap); O(RPOP); CALL(w_swap); O(SEMI);
/* : 0< -if L1 drop -1 ; L1: drop 0 ; */
w_zless = v4_asm_label(&as);
ref = v4_asm_branch_fwd(&as, V4_OP_MINUS_IF);
O(DROP); LIT(-1); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); LIT(0); O(SEMI);
/* : 0= if L1 drop 0 ; L1: drop -1 ; */
w_zequal = v4_asm_label(&as);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
O(DROP); LIT(0); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); LIT(-1); O(SEMI);
/* : 2DUP over over ; (section 5.1) */
w_2dup = v4_asm_label(&as);
O(OVER); O(OVER); O(SEMI);
/* : - NEGATE + ; (section 5.4), NEGATE in line */
w_minus = v4_asm_label(&as);
O(INV); LIT(1); O(ADD); O(ADD); O(SEMI);
/* : U< 2DUP xor 0< IF NIP 0< ELSE - 0< THEN ;
* with section 2's IF: the flag is dropped on both arms. */
w_uless = v4_asm_label(&as);
CALL(w_2dup); O(XOR); CALL(w_zless);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
O(DROP); CALL(w_nip); CALL(w_zless); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); CALL(w_minus); CALL(w_zless); O(SEMI);
/* : UM* a! 0 31 FOR +* UNEXT push drop a pop ;
* 31 is the cell width less one; written for a 32-bit cell in section 4.
* The loop body is the start of its own word so that unext restarts it. */
w_umstar = v4_asm_label(&as);
O(BANG_A); LIT(0); LIT(V4_CELL_BITS - 1); O(PUSH);
(void)v4_asm_label(&as);
O(MUL_STEP); O(UNEXT); O(PUSH); O(DROP); O(PUSH_A); O(RPOP);
O(SEMI);
CHECK(v4_asm_ok(&as), "foundation words assemble");
}
/* Call `word` with a canary and up to three arguments on fresh stacks. */
static int call(v4_cell word, unsigned argc, v4_cell a, v4_cell b, v4_cell c)
{
v4_dstack_reset(&n.ds);
v4_rstack_reset(&n.rs);
v4_exec_reset(&es);
v4_heat_reset(&h);
v4_dstack_push(&n.ds, CANARY);
if (argc > 0) v4_dstack_push(&n.ds, a);
if (argc > 1) v4_dstack_push(&n.ds, b);
if (argc > 2) v4_dstack_push(&n.ds, c);
return v4_test_call(&n, &es, &h, word, 1000) > 0;
}
/* The results, top first, then the canary. */
static int left1(v4_cell t)
{
return n.ds.t == t && n.ds.s == CANARY;
}
static int left2(v4_cell s, v4_cell t)
{
if (n.ds.t != t || n.ds.s != s) return 0;
(void)v4_dstack_pop(&n.ds);
return n.ds.s == CANARY;
}
static int left3(v4_cell third, v4_cell s, v4_cell t)
{
if (n.ds.t != t) return 0;
(void)v4_dstack_pop(&n.ds);
return left2(third, s);
}
static const v4_cell vec[] = {
0, 1, 2, 3, -1, -2, 12345, -12345,
(v4_cell)(V4_MSB - 1u), (v4_cell)V4_MSB, (v4_cell)(V4_MSB + 1u),
(v4_cell)(V4_MSB >> 1), (v4_cell)((V4_MSB >> 1) - 1u),
(v4_cell)(MAXU / 3u), (v4_cell)(MAXU / 3u * 2u)
};
#define NVEC (sizeof vec / sizeof vec[0])
/* Does UM* as written give the true product? */
static int umstar_exact(v4_ucell u1, v4_ucell u2)
{
v4_ucell lo, hi;
v4_umul(u1, u2, &lo, &hi);
return call(w_umstar, 2, (v4_cell)u1, (v4_cell)u2, 0) && left2((v4_cell)lo, (v4_cell)hi);
}
int main(void)
{
printf("v4 foundation tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
build();
for (unsigned i = 0; i < NVEC; i++) {
v4_cell a = vec[i];
v4_ucell ua = (v4_ucell)a;
CHECK(call(w_negate, 1, a, 0, 0) && left1((v4_cell)(0u - ua)), "NEGATE [%u]", i);
CHECK(call(w_zless, 1, a, 0, 0) && left1(FLAG(ua & V4_MSB)), "0< [%u]", i);
CHECK(call(w_zequal, 1, a, 0, 0) && left1(FLAG(a == 0)), "0= [%u]", i);
for (unsigned j = 0; j < NVEC; j++) {
v4_cell b = vec[j];
v4_ucell ub = (v4_ucell)b;
CHECK(call(w_nip, 2, a, b, 0) && left1(b), "NIP [%u,%u]", i, j);
CHECK(call(w_swap, 2, a, b, 0) && left2(b, a), "SWAP [%u,%u]", i, j);
CHECK(call(w_or, 2, a, b, 0) && left1((v4_cell)(ua | ub)), "OR [%u,%u]", i, j);
CHECK(call(w_2dup, 2, a, b, 0) && n.ds.t == b && n.ds.s == a
&& (v4_dstack_pop(&n.ds), v4_dstack_pop(&n.ds), left2(a, b)),
"2DUP [%u,%u]", i, j);
CHECK(call(w_minus, 2, a, b, 0) && left1((v4_cell)(ua - ub)), "- [%u,%u]", i, j);
CHECK(call(w_uless, 2, a, b, 0) && left1(FLAG(ua < ub)), "U< [%u,%u]", i, j);
for (unsigned k = 0; k < NVEC; k++) {
v4_cell c = vec[k];
CHECK(call(w_rot, 3, a, b, c) && left3(b, c, a), "ROT [%u,%u,%u]", i, j, k);
}
/* UM* as written is exact whenever the multiplicand (u1, which
* stays in S) is at most 2^(n-2): T is then always below S, so
* T+S stays below 2^(n-1) and neither the lost carry nor +*'s
* sign-keeping shift can touch the result. u2 is unrestricted. */
if (ua <= (V4_MSB >> 1))
CHECK(umstar_exact(ua, ub), "UM* [%u,%u]", i, j);
}
}
/* KNOWN LIMITS of UM* as written, recorded so they cannot be forgotten.
* Section 4 marks UM* "NEEDS REVISION" and gives the limit as "operands
* below 2^31". The measured limit is tighter and is on u1 only. These
* two checks assert that the definition is still wrong where it is known
* to be wrong; when UM* is rewritten they must be turned into ordinary
* exactness checks. */
CHECK(!umstar_exact(MAXU, MAXU), "UM* limit: carry out of T is lost (D-3)");
CHECK(!umstar_exact(V4_MSB - 1u, 3u), "UM* limit: u1 below 2^(n-1) still fails");
CHECK(v4_node_guards_intact(&n), "guards intact");
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+213
View File
@@ -0,0 +1,213 @@
/* 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;
}
+109
View File
@@ -0,0 +1,109 @@
/* test_heat.c -- heat and anti-clock. */
#include "v4/heat.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_reset(void)
{
v4_heat h;
memset(&h, 0xFF, sizeof h);
v4_heat_reset(&h);
for (unsigned i = 0; i < V4_OPCODE_COUNT; i++) {
CHECK(h.op[i] == 0, "op %u heat non-zero after reset", i);
}
for (unsigned i = 0; i < V4_HEAT_MAX_CALL_TARGETS; i++) {
CHECK(h.call[i] == 0, "call target %u heat non-zero after reset", i);
CHECK(!v4_heat_call_is_frozen(&h, (v4_cell)i),
"call target %u frozen after reset", i);
}
v4_uheat_t ac = 999;
v4_heat_on_retire(&h, V4_OP_NOP, &ac);
CHECK(ac == 1000, "anticlock not advanced");
CHECK(h.op[V4_OP_NOP] == 1, "nop heat");
}
static void test_retire_advances_anticlock_and_opcode(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_uheat_t ac = 0;
for (int i = 0; i < 100; i++) {
v4_heat_on_retire(&h, V4_OP_ADD, &ac);
}
CHECK(ac == 100, "anticlock");
CHECK(h.op[V4_OP_ADD] == 100, "add heat");
for (unsigned i = 0; i < V4_OPCODE_COUNT; i++) {
if (i == V4_OP_ADD) continue;
CHECK(h.op[i] == 0, "other op %u", i);
}
}
static void test_call_advances_target(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 5);
CHECK(v4_heat_call_get(&h, 10) == 2, "target 10");
CHECK(v4_heat_call_get(&h, 5) == 1, "target 5");
CHECK(v4_heat_call_get(&h, 1) == 0, "target 1");
}
static void test_freeze(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 7);
v4_heat_call_freeze(&h, 7);
CHECK(v4_heat_call_is_frozen(&h, 7), "frozen");
v4_heat_on_call(&h, 7);
CHECK(v4_heat_call_get(&h, 7) == 1, "still 1");
CHECK(v4_heat_call_set(&h, 7, 100) == 0, "set returns 0 when frozen");
CHECK(v4_heat_call_get(&h, 7) == 1, "unchanged");
v4_heat_call_unfreeze(&h, 7);
CHECK(!v4_heat_call_is_frozen(&h, 7), "unfrozen");
CHECK(v4_heat_call_set(&h, 7, 42) == 1, "set allowed");
CHECK(v4_heat_call_get(&h, 7) == 42, "changed");
}
static void test_out_of_range_ignored(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5);
CHECK(v4_heat_call_get(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5) == 0,
"out of range call ignored");
CHECK(v4_heat_call_set(&h, (v4_cell)-1, 123) == 0, "set out of range");
CHECK(v4_heat_call_is_frozen(&h, (v4_cell)-1) == 0, "frozen check");
}
int main(void)
{
printf("v4 heat tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
test_reset();
test_retire_advances_anticlock_and_opcode();
test_call_advances_target();
test_freeze();
test_out_of_range_ignored();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More