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
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Build output. Regenerated by `make test` / `make sanitize`.
|
||||
build/
|
||||
+105
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user