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
+1 -1
View File
@@ -180,7 +180,7 @@ definition below depends on one, it says so.
| **D-16** | Stack overflow and underflow; `DEPTH`, `PICK`, `ROLL` (ruled 2026-10-04; revises D-2). | **Guarded: a stack fault.** Each stack counts what it holds. Before every opcode the node checks that the stacks hold what the opcode takes — including a `T` or `S` it only reads — and have room for what it leaves. If not, the opcode does nothing and the node faults exactly as for a bad address (D-14), to that kind's handler: `Stack overflow`, `Stack underflow`, `Return stack overflow`, `Return stack underflow`, then ` ERROR` and the prompt. **Every fault, D-14's included, empties both stacks**: the handler does not return, and what a word stopped part-way has left on the data stack is of no use to its caller. (v3 keeps what the failing word had not taken, and names the word: `DROP: Stack underflow`.) The fault handler is a table of five words, one per kind, each a jump (`(FAULTS)` in `v4/capsule/quit.v4`). Two registers (§7) are all a programme sees of the stacks: `DSTACK-DEPTH` and `RSTACK-DEPTH` read as the depth, and a store to one empties that stack. There is still no stack pointer and no address for a stack cell, so `SP@` and `SP!` stay retired; `.S` waits for number output to reach the capsule. The sizes were unchanged by this ruling, 10 and 9 (D-17 then deepens the host node's): one more value, or one more level of call, is now an error message where it used to be silent corruption. Executed on the golden model (2026-10-04): `tests/test_exec.c` for every opcode at every depth of both stacks, `tests/test_host_quit.c` from the prompt. |
| **D-17** | Stack sizes on the host node (2026-10-04, following D-16). | **32 values and 32 return entries on the host node; a mesh node keeps the F18's 10 and 9.** Once the stacks are counted (D-16) their size is a parameter of the node, like its memory, and the host node is the one that runs the interpreter and the compiler underneath the user's programme: at 10 and 9 the prompt left a programme about six values, and `/` could be used only four words deep. The mechanism is the same at both sizes — top registers over a ring — and so is every word's definition. v3's stacks are deeper still. In the golden model the sizes are `V4_DATA_RING` and `V4_RET_RING` (`stack.h`), set for the host-node tests in `v4/Makefile`. |
| **D-18** | Every other error (ruled 2026-10-04: "guard all errors"). | **A word that finds an error raises it, and the line ends there.** The word takes its own arguments off the stack and stores an error code in `NODE-ERROR` (§7). On a node with a prompt that store is a trap, a sixth kind of fault beside D-14's and D-16's: nothing after it executes, the return stack is emptied, and the prompt prints the code's message, ends any definition that was open, prints ` ERROR` and waits for the next line. Unlike the other faults it leaves the data stack as the word left it. The codes: 1 `Negative count` (`CMOVE`, `TYPE`), 2 `Not a number` (`NUMBER`), 3 `Number too long` (`HOLD` into a full buffer), 4 `Not a character` (`HOLD`), 5 `Dictionary full` (`,` `C,` `ALLOT`, any defining word), 6 `Name missing` (a defining word with nothing after it), 7 `Control structure mismatch`, 8 `Control structures too deep`, 9 `Shift count out of range`, 10 `Protected word`, 11 `Division by zero` (`Q./`), 12 `Argument out of range`, 13 `Block out of range`, 14 `No block is being loaded`, 15 `Deferred word not set`, 16 `Not a word`; −1 means the word printed its own message (`UNKNOWN WORD: 'xxx'`, `xxx: compile-only`). Before this ruling these set the flag and the line ran on to its end. v3 stops at once too; its messages name the word (`LOOP: missing DO`) where v4's name the fault. With the trap not attached `NODE-ERROR` is plain memory and the word returns, which is how the words are tested below the prompt. D-11, D-12 and D-13 said "set `NODE-ERROR`"; on a node with a prompt that now means this. Executed on the golden model (2026-10-04): `tests/test_exec.c`, `tests/test_host_quit.c`. |
| **D-19** | Block storage on the golden model (2026-10-05). **Replaced 2026-10-07:** the four registers are gone; a node asks its kernel for a block by a request on port 0 (`MESH.md` section 8, `v4/include/v4/blocks.h`). What follows is the decision as it was. | **Four memory-mapped registers on the host node, until the mesh carries the storage service.** `BLOCK-NUMBER`, `BLOCK-ADDRESS` (the word address of 256 cells), `BLOCK-COMMAND` (a store of 1 reads the block into those cells, 2 writes it from them) and `BLOCK-STATUS` (0 when the command worked, −1 for a block the device has not got or cells not all in memory). A block is 1024 bytes: 256 cells, four bytes to a cell, the first byte lowest, at either cell width. This is the console's arrangement (§7) applied to storage; like it, it is the model's stand-in and not the mesh protocol, which §5.11 still leaves to the device node. |
| **D-19** | Block storage on the golden model (2026-10-05). **Replaced 2026-10-07:** the four registers are gone; the block words are kernel requests on port 0, by block number, and the kernel keeps the node's window of four slots (`MESH.md` section 8, `v4/include/v4/blocks.h`). What follows is the decision as it was. | **Four memory-mapped registers on the host node, until the mesh carries the storage service.** `BLOCK-NUMBER`, `BLOCK-ADDRESS` (the word address of 256 cells), `BLOCK-COMMAND` (a store of 1 reads the block into those cells, 2 writes it from them) and `BLOCK-STATUS` (0 when the command worked, −1 for a block the device has not got or cells not all in memory). A block is 1024 bytes: 256 cells, four bytes to a cell, the first byte lowest, at either cell width. This is the console's arrangement (§7) applied to storage; like it, it is the model's stand-in and not the mesh protocol, which §5.11 still leaves to the device node. |
**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
+76 -60
View File
@@ -315,34 +315,57 @@ in 8.6, so that it is not proposed again.
### 8.2 What a node does *(step 6)*
`BLOCK`, `BUFFER`, `UPDATE` and `SAVE-BUFFERS` keep their two buffers and
their FORTH-79 behaviour. The one word beneath them in
`v4/capsule/blocks.v4` that read or wrote a block by storing to four
registers makes a kernel request instead: it puts the block's number and
the word address of the buffer's 256 cells on the data stack and writes the
request's number to port 0, where its kernel is. When the write has been
served the status is on the stack in their place: 0 it worked, 2 it was
refused, 3 there is no such block.
**Ruled 2026-10-07: v3's way in full.** The block words are the kernel's.
`BLOCK`, `BUFFER`, `UPDATE`, `SAVE-BUFFERS` and `EMPTY-BUFFERS` in
`v4/capsule/blocks.v4` each write one request to port 0, where the node's
kernel is, and do nothing else: `BLOCK` and `BUFFER` with the block's
number on the stack, the others with nothing. The node gives the kernel no
address and keeps no record of what is in its buffers.
There are two requests, read a block and write a block. Their numbers are
the same for every node and are below zero; the requests a host names for
its own words (`KERNEL-WORD`) count up from 1.
The node has a **window** in its memory, four slots of 256 cells, as a v3
VM has four slots of 1 KiB at the top of its memory. The kernel copies a
block into a slot and gives the node the slot's address.
The five requests' numbers are the same for every node and are below zero,
-1 to -5; the requests a host names for its own words (`KERNEL-WORD`)
count up from 1. The status a request leaves: 0 it worked, 2 it was
refused (error 17, "Storage refused"), 3 there is no such block (error 13).
The four storage registers and `v4_node_storage_attach` go from the engine.
### 8.3 What the kernel does *(step 6)*
Every node has its kernel on port 0, whatever else is there for it: Hera's
requests for nodes (section 9) are honoured only from Hera, and the two
block requests from every node.
requests for nodes (section 9) are honoured only from Hera, and the block
requests from every node.
`v4/system/blocks.c`, which the hosted and the bare-metal builds and the
tests share, serves them: it takes the number and the address from the
node's data stack, checks that the 256 cells are in the node's memory, and
reads or writes the block through v3's block subsystem
(`v3/src/block_subsystem.c`), four characters to a cell, the first lowest.
The engine (`v4/src`) knows nothing of it.
tests share, serves them, from v3's block subsystem
(`v3/src/block_subsystem.c`). For each node it keeps what v3 keeps for each
VM (`v3/src/word_source/block_words.c`): which block is in which slot of
the window, which have been UPDATEd, and the chain as it was when they
were filled. The engine (`v4/src`) knows nothing of it.
- It writes into the node's window and nowhere else in the node. That is
how a request cannot overwrite the node's code (ruled 2026-10-07: the
kernel refuses it): there is no address for a node to give.
- `BLOCK` gives the slot a block is in, and reads it from storage only if
it is in none. `BUFFER` gives a slot of zeros without reading.
- **When a block is written is FORTH-79's:** `UPDATE` marks it, and it is
written by `SAVE-BUFFERS` or when its slot is wanted. v3's `UPDATE`
copies the slot to the kernel at once; the standard is followed
(standing ruling). A slot whose block is not marked is taken before one
whose block is.
- A block storage will not take is let go of, and its slot is empty.
- When the chain of devices changes, everything in the slots is let go and
none of it is written, as v3 does.
- Four characters to a cell, the first lowest; a read leaves the rest of
each cell zero and a write takes the low 32 bits.
- **Each node has its own copy of a block it holds, as each v3 VM has.**
If node A has block *n* in a slot and node B writes it, A goes on reading
what it had, and A's next `UPDATE` and `SAVE-BUFFERS` writes all of the
block over B's. v3 is the same. Closing that would be a departure from
v3 and is not ruled.
- v3's code does what it does: header, allocation map, block cards,
relocation, three blocks and their cards to a 4 KiB device block.
- There is one chain, as in v3: fast RAM at blocks 0 to 2047, then the
@@ -376,37 +399,20 @@ block number not to exist, and on the real chain it does.
step 6a.
- **Cloud stores and real USB drives** wait for their drivers.
### 8.5 Open, for ruling
### 8.5 Ruled 2026-10-07, after the review of step 6
Found by review of step 6 on 2026-10-07; nothing is built for either.
The review found that a block request could be made by hand with an
address in the node's own code, and said that a node's own copy of a
block was a departure from v3. The second was wrong: a v3 VM has a window
of four slots in its own memory and `BLOCK` copies the kernel's block into
one (`v3/include/vm.h`, `BLK_VM_SLOTS`; `block_words.c`, `blk_vm_load`).
It was reported to Captain Bob as a departure before it was checked
against v3, and he ruled on it as one; it was then checked and taken back.
- **A node keeps its own copy of a block.** `BLOCK` gives the address of
one of the node's two buffers, and the kernel is asked only when a block
is not in one. If node A has block *n* in a buffer and node B writes it,
A goes on reading what it had; and if A then does `UPDATE` and
`SAVE-BUFFERS`, all of B's block is overwritten.
**Corrected 2026-10-07.** This entry first said that in v3 a VM's `BLOCK`
gives the kernel's buffer, so that every VM sees one copy. That was
wrong, and was not checked against v3 before it was written. In v3 each
VM has a window of four 1 KiB slots at the top of its own memory
(`v3/include/vm.h`, `BLK_VM_SLOTS`; `v3/src/word_source/block_words.c`,
`blk_vm_load`): `BLOCK` copies the kernel's block into a slot, unless
the block is already in one, and gives the slot's address. So a v3 VM
keeps its own copy too, and two VMs can each hold a stale one, exactly
as here. Where v4 differs from v3:
- two buffers, where v3 has four slots;
- the node keeps the record of what is in them and does the asking,
where in v3 the block words are the kernel's and the kernel keeps the
record and does the copying, only ever into the window;
- `UPDATE` marks the buffer and it is written when the buffer is wanted
or at `SAVE-BUFFERS`, as FORTH-79 has it, where v3's `UPDATE` copies
the slot to the kernel at once;
- v3 lets go of every slot when the device chain changes; v4 does that
in one place only, after POST on bare metal.
- **A block read over the node's own code.** The kernel checks that the
256 cells are in the node's memory and nothing more, so a request made
by hand with an address in the nucleus overwrites it and the node stops.
`!` can do the same; whether the kernel should refuse it is not ruled.
His rulings, on what v3 does: **the kernel refuses a request that would
overwrite the node's code**, and **v3's way in full** — the block words
are the kernel's, by number only, with the kernel keeping the record of
the window and doing the copying. Both are built: 8.2 and 8.3.
### 8.6 Withdrawn 2026-10-07
@@ -628,17 +634,23 @@ Each is tested, committed and pushed before the next.
6. **Storage (section 8): every node asks the kernel for its blocks.**
**Done 2026-10-07.** It was first built another way and brought back;
8.6 says what was withdrawn and why.
- *The requests* (`v4/include/v4/blocks.h`, `v4/system/blocks.c`). -1
reads a block and -2 writes one, `( n waddr -- status )`, for any
node, from the kernel's block subsystem. `v4/tests/test_blocks.c`: 22
checks at 64 bits, 21 at 32 — a block written and read, the fast RAM,
numbers that are no block, cells that are not in the node's memory,
and that what v3's own calls wrote is what the node reads.
- *The node* (`v4/capsule/blocks.v4`). `(DEVICE)` makes the request on
port 0. The four storage registers and `v4_node_storage_attach` are
gone from the engine. Error 17 is "Storage refused".
`v4/tests/test_host_quit.c`: its block cases unchanged, and a disk
that will not be written says so and empties the buffer.
- *The requests* (`v4/include/v4/blocks.h`, `v4/system/blocks.c`). Five,
-1 to -5: `BLOCK`, `BUFFER`, `UPDATE`, `SAVE-BUFFERS`,
`EMPTY-BUFFERS`, for any node, by block number only. The kernel keeps
the node's window of four slots. `v4/tests/test_host_blocks.c`: 41
checks at 64 bits, 39 at 32 — a block copied into a slot and found
there again, `UPDATE` and when a block is written, `EMPTY-BUFFERS`,
`BUFFER`, more blocks than slots, numbers that are no block, too
little on the stack, a window not in the node's memory, and the chain
changing under what the slots hold.
- *The node* (`v4/capsule/blocks.v4`). Each block word is one request
on port 0; the node's own buffers and its record of them are gone, and
so are the four storage registers and `v4_node_storage_attach`. Error
17 is "Storage refused". The window takes 512 cells more than the two
buffers did, and the dictionary space ends 512 cells lower, at 13824.
`v4/tests/test_host_quit.c`: its block cases, those that counted on
two buffers rewritten for four slots, and a disk that will not be
written says so and is let go of.
- *v3* (`v3/src/block_subsystem.c`). `blk_subsys_init` takes no `VM`.
Nothing else of it is changed. Accepted on the v3 configuration, three
ISAs, `PARITY:M7.1a` hash `0x08873e0f44b7cb2a` as on 2026-10-03:
@@ -658,7 +670,9 @@ Each is tested, committed and pushed before the next.
a block that is not there. `logs/20261007-081603` (amd64), `-081839`
(aarch64), `-082226` (riscv64); and again after the review's changes
below, with blocks 1 and 2047 read clean at the prompt:
`logs/20261007-085017`, `-085254`, `-085636`. The parity hashes are the same on the
`logs/20261007-085017`, `-085254`, `-085636`; and as it stands, with the
block words the kernel's: `logs/20261007-092835`, `-093112`,
`-093456`. The parity hashes are the same on the
three hosted and the three bare-metal systems.
- *Review, 2026-10-07.* One reading of the whole step by a fresh
reviewer; no critical defect. Changed after it: a block request with
@@ -669,7 +683,9 @@ Each is tested, committed and pushed before the next.
`blocks.c` is built under each test's own warnings and sanitizers;
the hosted link no longer takes whatever objects lie in its
directory (it had linked the withdrawn `store_v3.o`). Two findings
are for ruling, section 8.5.
went to Captain Bob, and his rulings on them are section 8.5; they
made the block words the kernel's, which replaced the request first
built here, `( n waddr -- status )`.
- **Not as intended yet.**
- Who may have which block is not checked (8.4).
- On bare metal the virtio disk is read and not written. v3's
+4 -3
View File
@@ -188,9 +188,10 @@ array behind its four block registers (D-19), which skips the address
space, the metadata, the owner, the ACL and the Stadium.
**As of 2026-10-07** (`MESH.md` step 6): the registers and the RAM array
are gone. A node's block words make a kernel request and
`v4/system/blocks.c` serves it through v3's block subsystem, so the address
space and the metadata are v3's. Still skipped: the owner and first-touch
are gone. A node's block words are kernel requests by block number, and
`v4/system/blocks.c` serves them through v3's block subsystem into the
node's window of four slots, as v3's block words do for a VM; so the
address space and the metadata are v3's. Still skipped: the owner and first-touch
claim, the ACL, and the Stadium touch, which need the node's identity. A
first build of that step had block requests passed from node to node and
nodes with no storage; Captain Bob withdrew it as a divergence from this