feat(v4.0.0): five nodes from nothing -- Hera asks for nodes, births and joins them

MESH.md step 5, in the fabric under test; the products are still one
node each until steps 8 and 9.

manage.c: what Hera asks of whoever holds the fabric, eleven requests by
KERNEL-WORD -- NODE-ME -BORN -WIRE -UNWIRE -SLEEP -WAKE -KILL -PARITY and
CAPSULE-OPEN -CELL -LINE. Nucleus: PORT!, SEND-ON, AWAIT, (SEAL).
capsules/v4/hera.4th: BIRTH and UNIT, the unit rule in FORTH and nowhere
else. The dictionary hash moves to the engine (v4_image_dict_hash).

test_host_unit.c, 34 checks, 64-bit: Hera is born empty, takes the
nucleus through her port, FORTH-79, POST and her capsule; 10 UNIT; four
nodes are born, each takes the nucleus and FORTH-79 through its port from
Hera and passes POST 538/538; four parities, one dictionary hash; they
talk, and a message between two corners not wired goes by Hera.

All v4 tests at both widths and under ASan+UBSan. hosted-check on three
ISAs. Bare metal: logs/20261006-143934 (amd64), -144204 (aarch64),
-144601 (riscv64).

Open, recorded in MESH.md: not run at 32 bits; a node that never answers
leaves Hera waiting; the capsules are read from files in the test, not
from the baked directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-06 14:48:23 -04:00
co-authored by Claude Opus 5.5
parent f9c034cadd
commit 348eed7fa1
18 changed files with 1250 additions and 40 deletions
+9
View File
@@ -123,6 +123,15 @@ read -- or, of two neighbours, the one with the lower number may wait.
`NEIGHBOUR ( node port -- )` tells a node who is on each port. A node with
no room for one more message lets it go and counts it in `(LOST)`.
**Five nodes from nothing** (`MESH.md` step 5) is built and proven in the
fabric under test: Hera asks whoever holds the fabric for a node
(`v4/src/manage.c`: `NODE-BORN`, `NODE-WIRE` and the rest), sends it the
nucleus through the port that joins them, then FORTH-79 and POST a line at
a time, and joins four such nodes in a square round her
(`capsules/v4/hera.4th`: `BIRTH`, `UNIT`). `v4/tests/test_host_unit.c`.
The two products are not yet this: they become the five nodes at steps 8
and 9.
**This is still the lone node.** On bare metal `kernel_main.c` starts it
before the fleet tables, beside the kernel's own system and not in the VM's
place. `ENGINE.md` sets out the steps from here; step 1 is done and step 2 is
+12 -4
View File
@@ -181,13 +181,18 @@ header CMOVE
SET: (MQ-HEAD) a! !
(MQ#) a! @ -1 + ! ;
\ ( port to -- ) take in a message. B is at the port, by its address,
\ and `to` is the message's first word, already read from it. A length
\ ( port to -- ) the seven words of a message being taken in, and the
\ port, go into (MQ-HDR). B is at the port, by its address, and `to` is
\ the message's first word, already read from it.
: (TAKE-HDR)
(MQ-HDR) a! !+ 5 FOR @b !+ UNEXT ! ;
\ ( -- ) the message whose seven words are in (MQ-HDR) is kept with the
\ messages waiting: its text is read from the port B is at. A length
\ below zero, or above what a message carries, is kept as -1 with no text:
\ a longer one is read to its end first, so that what follows it is not
\ taken for a message.
: (TAKE)
(MQ-HDR) a! !+ 5 FOR @b !+ UNEXT ! \ its seven words, and the port
: (TAKE-KEEP)
(MQ-HDR)+6 a! @ -if SIZED
drop -1 (MQ-HDR)+6 a! ! 0 jump COUNTED
SIZED: dup -1025 + -if LONG drop 3 + 2/ 2/ jump COUNTED
@@ -203,6 +208,9 @@ header CMOVE
GONE: drop
LOSE: (LOST) a! @ 1 + ! ;
\ ( port to -- ) take in a message and keep it
: (TAKE) (TAKE-HDR) jump (TAKE-KEEP)
\ ( port -- ) wait until a message may be begun on the port, by its
\ address, taking in whatever is being written to this node meanwhile; and
\ leave B at the port. The port is kept in (GATE-PORT), not on the stack:
+56 -7
View File
@@ -175,13 +175,11 @@ header DEFAULT-ROUTE
header NO-ROUTES
: NO-ROUTES 0 (ROUTE#) a! ! 0 (ROUTE-DEFAULT) a! ! ;
\ ( baddr u node -- ) send the node u characters of text to interpret. It
\ is on its way when SEND returns; what it prints there goes to that node's
\ console. With no way to the node it is error 12. The write waits for the
\ neighbour to read, and is not begun until it may be (core.v4, (GATE)).
header SEND
: SEND
dup (PORT-FOR) if NOWAY
\ ( baddr u node port -- ) send the node u characters of text to
\ interpret, by the port given, by its address. It is on its way when this
\ returns. The write waits for the neighbour to read, and is not begun
\ until it may be (core.v4, (GATE)).
: (SEND-ON)
(GATE) !b \ to
1 (HDR)
dup !b \ length
@@ -189,8 +187,59 @@ header SEND
FOR dup C@ over 1 + C@ 8* + over 2 + C@ 8* 8* + over 3 + C@ 8* 8* 8* + !b 4 + NEXT
drop ;
NONE: drop drop ;
\ ( baddr u node -- ) send the node u characters of text to interpret,
\ by the way this node knows to it. What it prints there goes to that
\ node's console. With no way to the node it is error 12.
header SEND
: SEND
dup (PORT-FOR) if NOWAY jump (SEND-ON)
NOWAY: drop drop drop drop NODE-ERROR b! 12 !b ;
\ ( baddr u node port -- ) the same by a port this node names, by its
\ number, whatever ways it knows: for a neighbour that has no number of
\ its own yet, to which text is sent as to node 0.
header SEND-ON
: SEND-ON
dup -if POS jump BAD
POS: -PORTS + -if BAD drop (PORT) + jump (SEND-ON)
BAD: drop drop drop drop drop NODE-ERROR b! 12 !b ;
\ ( node -- how ) WAIT FOR THE NODE'S WORD OF HOW TEXT ENDED: 0 QUIT,
\ 1 completed, 2 an error. This node is blocked reading its ports until a
\ message of type 3 comes for it from that node; every other message that
\ comes meanwhile is kept with the messages waiting. It waits for ever
\ for a node that never answers.
header AWAIT
: AWAIT
(AWAIT-FROM) a! !
L: (PORT)+8 b! @b (PORT)+9 b! @b (PORT) + dup b! SWAP (TAKE-HDR)
(MQ-HDR) a! @ (ME) a! @ xor if A1 drop jump KEEP
A1: drop (MQ-HDR)+2 a! @ -3 + if A2 drop jump KEEP
A2: drop (MQ-HDR)+1 a! @ (AWAIT-FROM) a! @ xor if A3 drop jump KEEP
A3: drop (MQ-HDR)+6 a! @ -4 + if A4 drop jump KEEP
A4: drop @b ; \ its one word
KEEP: (TAKE-KEEP) jump L
\ ( w port -- ) write a cell to the port, by its number. It waits until
\ the neighbour has taken it. This is how a node is sent a capsule of F18
\ code (docs/v4.0.0/MESH.md section 5): the cells go as they are, not as a
\ message, to a node that is executing from its ports.
header PORT!
: PORT!
dup -if POS jump BAD
POS: -PORTS + -if BAD drop (PORT) + b! !b ;
BAD: drop drop drop NODE-ERROR b! 12 !b ;
\ ( -- ) WHAT IS IN THE DICTIONARY NOW IS THE SYSTEM: COLD comes back to
\ it and FORGET will not go below it. Whoever has loaded a node's capsules
\ has it do this (system.v4, (BOOT) and FENCE).
header (SEAL)
: (SEAL)
DP a! @ (BOOT) a! !
(LATEST) a! @ (BOOT)+1 a! !
DP a! @ 3 + 2/ 2/ FENCE a! ! ;
\ FORTH-79: clear the return stack, set execution mode, return control to
\ the terminal; no message is given. The data stack is left as it is.
\ NODE-ERROR, by name: how a definition written in FORTH raises an error.
+6
View File
@@ -65,6 +65,12 @@ extern const v4_image v4_capsule_image;
* back to, and FORGET will not go below it. */
void v4_image_seal(v4_node *n, const v4_image *im);
/* The dictionary hash of a node started from `im`: FNV-1a over its memory
* below HERE, a cell at a time, low byte first, and then LATEST. It does
* not depend on the machine: every build of one commit, with one cell
* width, gives the same hash for the same dictionary. */
uint64_t v4_image_dict_hash(const v4_node *n, const v4_image *im);
/* A node newly born (node.h), with the registers the nucleus expects of the
* node it runs on -- and `disk`, `blocks` blocks of 1024 bytes, as its block
* storage, or none if `disk` is 0. Its memory is empty and it is blocked
+81
View File
@@ -0,0 +1,81 @@
/* manage.h -- what Hera asks of whoever holds the fabric.
* docs/v4.0.0/MESH.md section 9.
*
* Hera decides which nodes exist and are awake (MESH.md ruling 7). She is
* a node like any other and cannot make one: she asks, by a write to the
* port where her requests go (ENGINE.md 3.3), and the write blocks her until
* she is answered. Each request is a word on her, made with KERNEL-WORD
* (v4/capsule/compile.v4); its arguments are what it finds on her data
* stack and its results are what is left there.
*
* These are the requests, in order. The n-th is request `first + n`, where
* `first` is whatever number the host gives the first of them:
*
* NODE-ME ( -- place ) the place of the node that asks
* NODE-BORN ( -- place | -1 ) a node is born, empty, listening at
* its ports; -1: there is no room
* NODE-WIRE ( a pa b pb -- flag ) port pa of the node at a is wired
* to port pb of the node at b
* NODE-UNWIRE ( a pa -- ) what is on that port is cut
* NODE-SLEEP ( place -- ) it executes nothing until woken
* NODE-WAKE ( place -- )
* NODE-KILL ( place -- ) it is removed, whatever it was doing
* NODE-PARITY ( place number -- ) the host records the node's parity:
* its number and its dictionary hash
* CAPSULE-OPEN ( baddr u -- flag ) the capsule of that name is found
* and checked, and is read from its
* start; 0: there is none, or it is
* not what it says it is
* CAPSULE-CELL ( -- w flag ) the next cell of a capsule of F18
* code; 0 0 when there are no more
* CAPSULE-LINE ( baddr -- u | -1 ) the next line of a capsule of
* FORTH blocks, put at baddr: at
* most 64 characters; -1 at its end
*
* A place is where a node is in the fabric (fabric.h); it is the host's
* name for the node. A node's number, which messages are addressed by, is
* the nodes' own affair: Hera gives a newborn its number by telling it.
*
* Only the node the host has wired to this is answered, so only Hera's
* requests are honoured. The unit rule (MESH.md ruling 5) is not here: it
* is what Hera does with these, in her capsule.
*
* Nothing here uses the C library: the bare-metal kernel links this file.
*/
#ifndef V4_MANAGE_H
#define V4_MANAGE_H
#include "v4/fabric.h"
#define V4_MANAGE_WORDS 11
extern const char *const v4_manage_words[V4_MANAGE_WORDS];
#define V4_MANAGE_LINE 64 /* the most characters in a line of a block */
typedef struct {
v4_fabric *f;
/* A node is born: the host finds it room, adds it to the fabric and
* gives it what a node's hardware gives it. Its place, or -1. */
int (*born)(void *self);
/* The node that was at `place` has been removed: its room is free. */
void (*gone)(void *self, unsigned place, v4_fabric_node *was);
/* The capsule called `name`, `len` characters: found, its hash and
* signature checked, and to be read from its start. 1, or 0. */
int (*open)(void *self, const char *name, unsigned len);
/* The next cell of the open capsule: 1 with it in *w, 0 at its end. */
int (*cell)(void *self, v4_cell *w);
/* The next line of the open capsule that is part of a block, into
* `text`, at most V4_MANAGE_LINE characters: its length, or -1. */
int (*line)(void *self, char *text);
/* The node at `place`, which Hera calls `number`: record its parity. */
void (*parity)(void *self, unsigned place, v4_cell number, const v4_node *n);
void *self;
} v4_manage;
/* The node at `place` has written request `n` (0 is NODE-ME) to the port
* its requests go to. Its arguments are taken from its data stack and its
* results left there. Returns 1; 0 if `n` is not one of the requests, and
* then nothing has been done. */
int v4_manage_serve(const v4_manage *m, unsigned place, unsigned n);
#endif /* V4_MANAGE_H */
+6 -1
View File
@@ -180,8 +180,13 @@ unsigned v4_fabric_step(v4_fabric *f)
{
unsigned i, done = 0;
/* a node that is blocked fetches nothing, and is looked about for when
* it is next to execute */
for (i = 0; i < f->capacity; i++)
if (awake(f, i)) look_about(f, i);
if (awake(f, i)) {
const v4_node *n = &f->place[i].node->n;
if (!n->stopped && !n->asking && !(n->reading && !n->given)) look_about(f, i);
}
for (i = 0; i < f->capacity; i++)
if (awake(f, i)) done += v4_exec_step_word(&f->place[i].node->n, &f->place[i].node->es, &f->place[i].node->h);
+27
View File
@@ -35,3 +35,30 @@ void v4_image_seal(v4_node *n, const v4_image *im)
n->mem[im->boot_cells + 1] = n->mem[im->latest];
n->mem[im->fence] = (n->mem[im->dp] + 3) / 4;
}
/* FNV-1a over the node's memory below HERE, a cell at a time, low byte
* first, and then LATEST. */
#define FNV_OFFSET 0xcbf29ce484222325ULL
#define FNV_PRIME 0x00000100000001b3ULL
static uint64_t hash_cell(uint64_t h, v4_cell c)
{
v4_ucell u = (v4_ucell)c;
unsigned i;
for (i = 0; i < V4_CELL_BITS / 8; i++) {
h ^= (uint64_t)((u >> (8 * i)) & 0xffu);
h *= FNV_PRIME;
}
return h;
}
uint64_t v4_image_dict_hash(const v4_node *n, const v4_image *im)
{
uint64_t h = FNV_OFFSET;
v4_cell here = (n->mem[im->dp] + 3) / 4, k;
if (here < 0) here = 0;
if (here > (v4_cell)V4_NODE_WORDS) here = (v4_cell)V4_NODE_WORDS;
for (k = 0; k < here; k++) h = hash_cell(h, n->mem[k]);
return hash_cell(h, n->mem[im->latest]);
}
+107
View File
@@ -0,0 +1,107 @@
/* manage.c -- what Hera asks of whoever holds the fabric. See manage.h.
*
* Nothing here uses the C library: the bare-metal kernel links this file.
*/
#include "v4/manage.h"
const char *const v4_manage_words[V4_MANAGE_WORDS] = {
"NODE-ME", "NODE-BORN", "NODE-WIRE", "NODE-UNWIRE", "NODE-SLEEP", "NODE-WAKE", "NODE-KILL", "NODE-PARITY",
"CAPSULE-OPEN", "CAPSULE-CELL", "CAPSULE-LINE"
};
/* A character of the node's memory: four to a cell, the first lowest, as
* the nucleus keeps text (v4/capsule/core.v4). */
static int in_memory(v4_cell baddr) { return baddr >= 0 && (baddr >> 2) < (v4_cell)V4_NODE_WORDS; }
static char char_at(const v4_node *n, v4_cell baddr)
{
return (char)(((v4_ucell)n->mem[baddr >> 2] >> (8 * (unsigned)(baddr & 3))) & 0xFFu);
}
static void char_put(v4_node *n, v4_cell baddr, char c)
{
unsigned shift = 8 * (unsigned)(baddr & 3);
v4_ucell cell = (v4_ucell)n->mem[baddr >> 2];
cell = (cell & ~((v4_ucell)0xFFu << shift)) | ((v4_ucell)(unsigned char)c << shift);
n->mem[baddr >> 2] = (v4_cell)cell;
}
static unsigned place_of(v4_cell x) { return x < 0 ? ~0u : (unsigned)x; }
int v4_manage_serve(const v4_manage *m, unsigned place, unsigned request)
{
v4_fabric_node *asker = v4_fabric_node_at(m->f, place);
v4_node *n;
v4_cell a, b, c, d;
if (!asker || request >= V4_MANAGE_WORDS) return 0;
n = &asker->n;
switch (request) {
case 0: /* NODE-ME ( -- place ) */
v4_dstack_push(&n->ds, (v4_cell)place);
break;
case 1: /* NODE-BORN ( -- place | -1 ) */
v4_dstack_push(&n->ds, (v4_cell)(m->born ? m->born(m->self) : -1));
break;
case 2: /* NODE-WIRE ( a pa b pb -- flag ) */
d = v4_dstack_pop(&n->ds); c = v4_dstack_pop(&n->ds); b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
v4_dstack_push(&n->ds, v4_fabric_wire(m->f, place_of(a), place_of(b), place_of(c), place_of(d)) ? -1 : 0);
break;
case 3: /* NODE-UNWIRE ( a pa -- ) */
b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
(void)v4_fabric_unwire(m->f, place_of(a), place_of(b));
break;
case 4: /* NODE-SLEEP ( place -- ) */
a = v4_dstack_pop(&n->ds);
v4_fabric_sleep(m->f, place_of(a));
break;
case 5: /* NODE-WAKE ( place -- ) */
a = v4_dstack_pop(&n->ds);
v4_fabric_wake(m->f, place_of(a));
break;
case 6: { /* NODE-KILL ( place -- ) */
v4_fabric_node *was;
a = v4_dstack_pop(&n->ds);
if (place_of(a) == place) break; /* not itself: there would be no one to answer */
was = v4_fabric_remove(m->f, place_of(a));
if (was && m->gone) m->gone(m->self, place_of(a), was);
break;
}
case 7: { /* NODE-PARITY ( place number -- ) */
v4_fabric_node *it;
b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
it = v4_fabric_node_at(m->f, place_of(a));
if (it && m->parity) m->parity(m->self, place_of(a), b, &it->n);
break;
}
case 8: { /* CAPSULE-OPEN ( baddr u -- flag ) */
char name[64];
unsigned i, len;
b = v4_dstack_pop(&n->ds); a = v4_dstack_pop(&n->ds);
len = b < 0 ? 0u : (unsigned)b;
if (len == 0 || len > sizeof name || !in_memory(a) || !in_memory(a + (v4_cell)len - 1) || !m->open) {
v4_dstack_push(&n->ds, 0);
break;
}
for (i = 0; i < len; i++) name[i] = char_at(n, a + (v4_cell)i);
v4_dstack_push(&n->ds, m->open(m->self, name, len) ? -1 : 0);
break;
}
case 9: /* CAPSULE-CELL ( -- w flag ) */
a = 0;
if (m->cell && m->cell(m->self, &a)) { v4_dstack_push(&n->ds, a); v4_dstack_push(&n->ds, -1); }
else { v4_dstack_push(&n->ds, 0); v4_dstack_push(&n->ds, 0); }
break;
case 10: { /* CAPSULE-LINE ( baddr -- u | -1 ) */
char text[V4_MANAGE_LINE];
int len, i;
a = v4_dstack_pop(&n->ds);
len = m->line ? m->line(m->self, text) : -1;
if (len > V4_MANAGE_LINE) len = V4_MANAGE_LINE;
if (len > 0 && (!in_memory(a) || !in_memory(a + len - 1))) len = -1;
for (i = 0; i < len; i++) char_put(n, a + i, text[i]);
v4_dstack_push(&n->ds, (v4_cell)len);
break;
}
}
return 1;
}
+1 -21
View File
@@ -56,29 +56,9 @@ static void say_hex(const v4_boot *b, uint64_t v)
/* ---- hashing: FNV-1a, 64 bits, as kernel/src/vm/parity.c ----------------- */
#define FNV_OFFSET 0xcbf29ce484222325ULL
#define FNV_PRIME 0x00000100000001b3ULL
static uint64_t hash_cell(uint64_t h, v4_cell c)
{
v4_ucell u = (v4_ucell)c;
unsigned i;
for (i = 0; i < V4_CELL_BITS / 8; i++) {
h ^= (uint64_t)((u >> (8 * i)) & 0xffu);
h *= FNV_PRIME;
}
return h;
}
uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im)
{
uint64_t h = FNV_OFFSET;
v4_cell here = (n->mem[im->dp] + 3) / 4, k;
if (here < 0) here = 0;
if (here > (v4_cell)V4_NODE_WORDS) here = (v4_cell)V4_NODE_WORDS;
for (k = 0; k < here; k++) h = hash_cell(h, n->mem[k]);
return hash_cell(h, n->mem[im->latest]);
return v4_image_dict_hash(n, im);
}
/* how many words FORTH holds: the list from LATEST, each entry's link in the
+2
View File
@@ -81,6 +81,7 @@
#define MQ_HEAD (BUF0_W - 35) /* the messages waiting to be dealt with: where the oldest begins, */
#define MQ_TAIL (BUF0_W - 36) /* where the next will go, */
#define MQ_COUNT (BUF0_W - 37) /* and how many cells they take */
#define AWAIT_FROM (BUF0_W - 39) /* the node whose word of how text ended is waited for */
#define GATE_PORT (BUF0_W - 38) /* the port address a message is about to be begun on */
#define NEAR (BUF0_W - 64) /* for each port, the number of the node on the other end; 0: not told */
#define MQ_HDR (BUF0_W - 56) /* a message being taken in: its seven words and the port it came on */
@@ -194,6 +195,7 @@ static int host_load(v4_text *tx, v4_node *n, const char *const *files, unsigned
v4_text_constant(tx, "-PORTS", -(v4_cell)V4_PORTS);
v4_text_constant(tx, "(NEAR)", NEAR);
v4_text_constant(tx, "(GATE-PORT)", GATE_PORT);
v4_text_constant(tx, "(AWAIT-FROM)", AWAIT_FROM);
v4_text_constant(tx, "(MQ)", MQ_W);
v4_text_constant(tx, "(MQ-END)", MQ_W + MQ_CELLS);
v4_text_constant(tx, "-MQ-ROOM", -(v4_cell)(MQ_CELLS - 7)); /* cells taken + a text's words + this is below zero while the message fits */
File diff suppressed because it is too large Load Diff