feat(v4.0.0): the block words are the kernel's -- v3's way in full

Ruled 2026-10-07.  BLOCK, BUFFER, UPDATE, SAVE-BUFFERS and EMPTY-BUFFERS
are each one kernel request on port 0, by block number only.  The node
has a window of four slots in its memory, as a v3 VM has; the kernel
copies blocks into it, keeps the record of which block is in which slot,
and decides when a block is written (v4/system/blocks.c).  The node keeps
no record and gives the kernel no address, so a request cannot overwrite
the node's code.  When a block is written stays FORTH-79's: UPDATE marks
it.  When the chain of devices changes the slots are let go, as in v3.

The window is 512 cells more than the two buffers were; the dictionary
space ends that much lower, at 13824.

MESH.md 8.5 had said, from the review, that a v3 VM's BLOCK is the
kernel's buffer.  It is not, and the section now says what was reported,
what v3 does, and what was ruled.

make -C v4 test, sanitize and hosted-check pass; amd64, aarch64 and
riscv64 boot, POST 538 of 538, the same hashes on all six:
logs/20261007-092835, -093112, -093456.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 09:37:30 -04:00
co-authored by Claude Opus 5.5
parent 7f4d946d2e
commit 9bfd5071d2
21 changed files with 991 additions and 330 deletions
+23 -10
View File
@@ -78,7 +78,7 @@ static char out[V4_CONSOLE_CAP + 1];
#define REFUSING_FIRST "2112" /* its first block */
static unsigned char ram[RAM_BYTES], common_dev[DISK_BLOCKS * V4_BLOCK_BYTES], refusing_dev[REFUSING_BLOCKS * V4_BLOCK_BYTES];
#define disk (ram + (size_t)BLK_FORTH_SYS_RESERVED * BLK_FORTH_SIZE)
static unsigned long block_requests; /* how many the node has made */
static v4_blocks kblocks; /* what the kernel keeps of the node's block window */
extern void sf_time_init(void);
extern void log_set_level(int level);
@@ -122,7 +122,7 @@ static v4_cell last_request;
static void kernel_serve(void)
{
last_request = n.request;
if (v4_blocks_serve(&n, n.request)) { block_requests++; return; } /* a block, as for any node (blocks.h) */
if (v4_blocks_serve(&kblocks, &n, n.request)) return; /* a block, as for any node (blocks.h) */
if (n.request == 1) {
v4_cell xt = v4_dstack_pop(&n.ds);
if (known_n < 1024) known[known_n++] = xt;
@@ -266,6 +266,7 @@ static void boot_with(unsigned depth)
n.mem[SRC] = TIB;
for (v4_cell k = BVARS; k < BVARS + 14; k++) if (k != SRC) n.mem[k] = 0; /* empty buffers, SCR and BLK 0, no hook */
memset(ram, 0, sizeof ram); memset(common_dev, 0, sizeof common_dev);
v4_blocks_init(&kblocks, BUF0_W);
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
v4_node_fault_attach(&n, w_fault);
v4_node_error_attach(&n, NODE_ERROR);
@@ -1175,9 +1176,10 @@ int main(void)
boot();
CHECK(is(say(REFUSING_FIRST " BLOCK DROP UPDATE SAVE-BUFFERS 65 EMIT\n"), "Storage refused\n ERROR\nok> "), "a write storage will not take says so, and the prompt returns");
{
unsigned long before = block_requests;
CHECK(is(say(REFUSING_FIRST " BLOCK DROP\n"), " ok\nok> ") && block_requests == before + 1, "the buffer was emptied: the next BLOCK asks the kernel again");
CHECK(is(say(REFUSING_FIRST " BLOCK DROP\n"), " ok\nok> ") && block_requests == before + 1, "and the one after finds it in the buffer");
unsigned k, held = 0;
for (k = 0; k < V4_BLOCK_SLOTS; k++) if (kblocks.block[k] == 2112u) held++;
CHECK(held == 0, "the block storage would not take is let go: no slot holds it");
CHECK(is(say(REFUSING_FIRST " BLOCK C@ .\n"), "0 ok\nok> "), "and the next BLOCK reads it from storage again");
}
/* ---- blocks: text loaded from storage ---- */
@@ -1225,11 +1227,22 @@ int main(void)
"an UPDATEd block is not written yet");
CHECK(is(say("FLUSH\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'H' && disk[3 * V4_BLOCK_BYTES + 1] == ' ' && disk[3 * V4_BLOCK_BYTES + 1023] == 'I'
&& disk[2 * V4_BLOCK_BYTES + 1023] == 0 && disk[4 * V4_BLOCK_BYTES] == 0, "FLUSH writes it, all 1024 bytes and no others");
CHECK(is(say("74 3 BLOCK C! UPDATE 4 BLOCK DROP\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'H', "with two buffers a second block does not disturb it");
CHECK(is(say("5 BLOCK DROP\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'J', "a third takes its buffer, and it is written first");
CHECK(is(say("EMPTY-BUFFERS 74 3 BLOCK C! UPDATE 4 BLOCK DROP 5 BLOCK DROP 6 BLOCK DROP\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'H',
"with four slots three more blocks do not disturb it");
CHECK(is(say("7 BLOCK DROP\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'H', "a fifth takes the slot of a block that was not UPDATEd, and nothing is written");
CHECK(is(say("77 4 BLOCK C! UPDATE 77 5 BLOCK C! UPDATE 77 6 BLOCK C! UPDATE\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'H'
&& disk[4 * V4_BLOCK_BYTES] != 'M' && disk[5 * V4_BLOCK_BYTES] != 'M' && disk[6 * V4_BLOCK_BYTES] != 'M', "four UPDATEd blocks fill the slots, and none is written yet");
{
unsigned written;
CHECK(is(say("8 BLOCK DROP\n"), " ok\nok> "), "one more block is asked for");
written = (disk[3 * V4_BLOCK_BYTES] == 'J') + (disk[4 * V4_BLOCK_BYTES] == 'M') + (disk[5 * V4_BLOCK_BYTES] == 'M') + (disk[6 * V4_BLOCK_BYTES] == 'M');
CHECK(written == 1, "it takes the slot of one of them, and that one is written first: %u written", written);
}
CHECK(is(say("FLUSH\n"), " ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'J' && disk[4 * V4_BLOCK_BYTES] == 'M' && disk[5 * V4_BLOCK_BYTES] == 'M' && disk[6 * V4_BLOCK_BYTES] == 'M',
"FLUSH writes the rest");
CHECK(is(say("3 BLOCK C@ . 75 3 BLOCK C! UPDATE EMPTY-BUFFERS FLUSH 3 BLOCK C@ .\n"), "74 74 ok\nok> ") && disk[3 * V4_BLOCK_BYTES] == 'J',
"EMPTY-BUFFERS forgets a change without writing it");
CHECK(is(say("76 3 BLOCK C! 5 BLOCK DROP 6 BLOCK DROP 3 BLOCK C@ .\n"), "74 ok\nok> "), "a change without UPDATE is lost when the buffer is taken");
CHECK(is(say("76 3 BLOCK C! 5 BLOCK DROP 6 BLOCK DROP 7 BLOCK DROP 8 BLOCK DROP 3 BLOCK C@ .\n"), "74 ok\nok> "), "a change without UPDATE is lost when the slot is taken");
CHECK(is(say("3 BLOCK 4 BLOCK 1024 CMOVE UPDATE SAVE-BUFFERS\n"), " ok\nok> ") && memcmp(disk + 3 * V4_BLOCK_BYTES, disk + 4 * V4_BLOCK_BYTES, V4_BLOCK_BYTES) == 0
&& disk[4 * V4_BLOCK_BYTES] == 'J', "two blocks in memory at once: one copied to the other");
put_block(40, "this is on the device");
@@ -1238,7 +1251,7 @@ int main(void)
{
v4_cell k, wide = 0;
(void)say("3 BLOCK DROP\n");
for (k = 0; k < 2 * 256; k++) if ((v4_ucell)n.mem[BUF0_W + k] > 0xFFFFFFFFu) wide = 1;
for (k = 0; k < (v4_cell)(V4_BLOCK_SLOTS * V4_BLOCK_CELLS); k++) if ((v4_ucell)n.mem[BUF0_W + k] > 0xFFFFFFFFu) wide = 1;
CHECK(!wide, "a block's bytes are four to a cell at every cell width");
}
{
@@ -1396,7 +1409,7 @@ int main(void)
&& is(say("4 LOG-LEVEL!\n"), " ok\nok> "), "a vocabulary, a base, a block, a screen, a log level");
CHECK(is(say("1 2 COLD 65 EMIT\n"), "FORTH-79 Cold Start\nSystem initialized.\n ok\nok> "), "COLD says so");
CHECK(n.mem[DP] == dp && n.mem[LATEST] == capsule_latest && n.mem[CONTEXT] == LATEST && n.mem[CURRENT] == LATEST && n.mem[VOC_LINK] == 0
&& n.mem[BASE] == 10 && n.mem[SCR] == 0 && n.mem[BVARS] == 0 && n.mem[FENCE] == dp / 4 && n.mem[LOG_LEVEL] == 2, "and everything is as the loader left it");
&& n.mem[BASE] == 10 && n.mem[SCR] == 0 && kblocks.block[0] == 0 && kblocks.block[1] == 0 && kblocks.block[2] == 0 && kblocks.block[3] == 0 && n.mem[FENCE] == dp / 4 && n.mem[LOG_LEVEL] == 2, "and everything is as the loader left it");
CHECK(is(say("KEEPME\n"), "UNKNOWN WORD: 'KEEPME'\n ERROR\nok> ") && is(say("VV\n"), "UNKNOWN WORD: 'VV'\n ERROR\nok> ")
&& is(say(".S : NEW 16 . ; NEW\n"), "<0> \n16 ok\nok> "), "what was defined is gone, and the system works");
}