MESH.md step 2. A capsule of F18 code is the words a neighbour writes to a node's port: for each stretch of memory, "@p a! @p push", the address and count, "@p !+ unext" and the words; then a jump to the start. A node born empty executes that from its port, so it needs nothing in it beforehand. - capsule.h/.c: v4_capsule_write, any node's memory as such a capsule - mkimage writes the nucleus so, to capsules/v4/nucleus-64.f18, and the addresses a host needs as a C file; the memory image is no longer linked into either product - mkcapsule is unchanged: the nucleus capsule is a built file kept under capsules/, as BLOCK_MAP.md is, and is baked, hashed and signed with the rest - boot: the node is born empty (v4_image_born); the nucleus capsule is found, its hash and signature checked, and given to the node a word at a time as it reads its port; PARITY:V4_NUCLEUS carries its name and hash Verified: test_fabric.c (59 checks, both widths, and under ASan and UBSan): a memory with a programme and scattered words arrives word for word in an empty node and runs. The nucleus capsule rebuilds byte for byte. hosted-check passes on three ISAs; clean qemu with STARFORTH_V4=1 on amd64, aarch64 and riscv64 takes the nucleus in, passes POST (550 of 550) and answers lines typed at each prompt (logs/20261006-102421, -102706, -103048). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
337 lines
17 KiB
C
337 lines
17 KiB
C
/* test_fabric.c -- nodes that talk: ports that block on a read as well as a
|
|
* write, a node born empty that executes what arrives at its port, and the
|
|
* fabric of nodes and wiring that changes while they run.
|
|
* docs/v4.0.0/MESH.md section 4 and step 1.
|
|
*
|
|
* At the level of opcodes: no word of any vocabulary is defined here.
|
|
*/
|
|
#include "v4/asm.h"
|
|
#include "v4/capsule.h"
|
|
#include "v4/fabric.h"
|
|
#include <stdio.h>
|
|
#include <string.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)
|
|
|
|
/* The memory map is open (D-4); the test chooses where the ports are. */
|
|
#define PB ((v4_cell)(V4_NODE_WORDS - 16u)) /* port 0 */
|
|
#define ANY (PB + (v4_cell)V4_PORT_ANY)
|
|
#define ORG ((v4_cell)16) /* where a test's programme starts */
|
|
#define OUT ((v4_cell)900) /* where it leaves what it found */
|
|
|
|
#define NODES 8
|
|
static v4_fabric_node pool[NODES];
|
|
static v4_place places[4], more_places[NODES];
|
|
static v4_fabric f;
|
|
static v4_asm as;
|
|
|
|
/* the node with that number in the fabric */
|
|
#define ND(id) (v4_fabric_node_at(&f, (id)))
|
|
|
|
#define O(name) v4_asm_op(&as, V4_OP_##name)
|
|
#define LIT(v) v4_asm_lit(&as, (v4_cell)(v))
|
|
|
|
/* A node with a programme already in it, started at ORG: for the tests that
|
|
* are about talking and not about being born. */
|
|
static unsigned loaded(unsigned k)
|
|
{
|
|
int id = v4_fabric_add(&f, &pool[k], PB);
|
|
pool[k].n.p = ORG;
|
|
v4_asm_begin(&as, &pool[k].n, ORG);
|
|
return (unsigned)id;
|
|
}
|
|
|
|
/* the programme ends by waiting at a port nothing is wired to */
|
|
static void wait_for_ever(void) { LIT(PB + 7); O(BANG_B); O(FETCH_B); (void)v4_asm_label(&as); } /* and the last word is written out */
|
|
|
|
/* Let time pass until nothing more can happen, or `max` steps. */
|
|
static unsigned settle(unsigned max)
|
|
{
|
|
unsigned steps = 0;
|
|
while (steps < max && v4_fabric_step(&f) != 0) steps++;
|
|
return steps;
|
|
}
|
|
|
|
/* ---- a device: a neighbour that is not a node ---- */
|
|
typedef struct { const v4_cell *words; unsigned count, at; v4_cell took[16]; unsigned took_n; } stream;
|
|
static int stream_give(void *self, v4_cell *value)
|
|
{
|
|
stream *s = (stream *)self;
|
|
if (s->at >= s->count) return 0;
|
|
*value = s->words[s->at++];
|
|
return 1;
|
|
}
|
|
static int stream_take(void *self, v4_cell value)
|
|
{
|
|
stream *s = (stream *)self;
|
|
if (s->took_n >= 16) return 0;
|
|
s->took[s->took_n++] = value;
|
|
return 1;
|
|
}
|
|
|
|
/* for v4_capsule_write: each word of the capsule, into the stream's own list */
|
|
static void collect(void *self, v4_cell word)
|
|
{
|
|
stream *s = (stream *)self;
|
|
((v4_cell *)s->words)[s->count++] = word;
|
|
}
|
|
|
|
/* What a neighbour sends an empty node to put `count` words at `addr` and
|
|
* start there (MESH.md section 5): for the F18, and for this node, code a
|
|
* node executes straight from its port. */
|
|
static unsigned capsule(v4_cell *out, const v4_cell *words, unsigned count, v4_cell addr)
|
|
{
|
|
static const unsigned set_up[V4_SLOT_COUNT] = { V4_OP_FETCH_P, V4_OP_BANG_A, V4_OP_FETCH_P, V4_OP_PUSH, V4_OP_NOP, V4_OP_NOP };
|
|
static const unsigned pump[V4_SLOT_COUNT] = { V4_OP_FETCH_P, V4_OP_STORE_INC, V4_OP_UNEXT, V4_OP_NOP, V4_OP_NOP, V4_OP_NOP };
|
|
static v4_node scratch;
|
|
v4_asm j;
|
|
unsigned at = 0, i;
|
|
|
|
out[at++] = (v4_cell)v4_iword_assemble(set_up, 0); /* @p a! @p push */
|
|
out[at++] = addr;
|
|
out[at++] = (v4_cell)(count - 1);
|
|
out[at++] = (v4_cell)v4_iword_assemble(pump, 0); /* @p !+ unext */
|
|
for (i = 0; i < count; i++) out[at++] = words[i];
|
|
v4_node_reset(&scratch);
|
|
v4_asm_begin(&j, &scratch, 0);
|
|
v4_asm_branch(&j, V4_OP_JUMP, addr); /* jump addr */
|
|
(void)v4_asm_label(&j);
|
|
out[at++] = scratch.mem[0];
|
|
return at;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
unsigned a, b, c, i, steps;
|
|
|
|
printf("v4 fabric tests: V4_CELL_BITS=%d, %u ports to a node\n", V4_CELL_BITS, (unsigned)V4_PORTS);
|
|
|
|
/* ---- a node newly born ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
CHECK(v4_fabric_node_at(&f, 0) == 0 && v4_fabric_step(&f) == 0, "an empty fabric: no nodes, and nothing happens");
|
|
a = (unsigned)v4_fabric_add(&f, &pool[0], PB);
|
|
CHECK(a == 0 && v4_fabric_node_at(&f, 0) == &pool[0], "a node is born and has a number");
|
|
CHECK(pool[0].n.p == ANY && pool[0].n.ds.depth == 0 && pool[0].n.rs.depth == 0, "its P is at \"any port\" and its stacks are empty");
|
|
for (i = 0; i < V4_NODE_WORDS && pool[0].n.mem[i] == 0; i++) { }
|
|
CHECK(i == V4_NODE_WORDS, "and there is nothing in its memory");
|
|
CHECK(v4_fabric_step(&f) == 0 && pool[0].n.reading && pool[0].n.read_port == V4_PORT_ANY, "it is blocked reading its ports");
|
|
CHECK(v4_fabric_step(&f) == 0 && pool[0].es.anticlock == 0, "and executes nothing while it waits: its anti-clock has not moved");
|
|
CHECK(v4_fabric_remove(&f, a) == &pool[0] && v4_fabric_node_at(&f, 0) == 0, "it is removed, and its storage comes back");
|
|
|
|
/* ---- two nodes exchange words ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0); /* writes three words, then reads one */
|
|
LIT(PB + 2); O(BANG_B); LIT(10); O(STORE_B); LIT(20); O(STORE_B); LIT(12); O(STORE_B);
|
|
O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A);
|
|
wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "the writer assembles");
|
|
b = loaded(1); /* reads three, writes their sum back */
|
|
LIT(PB + 5); O(BANG_B); O(FETCH_B); O(FETCH_B); O(FETCH_B); O(ADD); O(ADD); O(STORE_B);
|
|
wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "the reader assembles");
|
|
|
|
steps = settle(1000);
|
|
CHECK(ND(a)->n.asking && ND(a)->n.ask_port == 2 && ND(a)->n.request == 10, "with no wire, the writer is blocked at its first write");
|
|
CHECK(ND(b)->n.reading && ND(b)->n.read_port == 5, "and the reader at its first read");
|
|
CHECK(v4_fabric_wire(&f, a, 2, b, 5), "port 2 of one is wired to port 5 of the other, while they wait");
|
|
steps = settle(1000);
|
|
CHECK(steps < 1000, "then everything that can happen happens, and stops: %u steps", steps);
|
|
CHECK(ND(a)->n.mem[OUT] == 42, "three words went one way and their sum came back: %ld", (long)ND(a)->n.mem[OUT]);
|
|
CHECK(ND(a)->n.reading && ND(b)->n.reading, "and both are waiting again");
|
|
{
|
|
v4_uheat_t ca = ND(a)->es.anticlock, cb = ND(b)->es.anticlock;
|
|
for (i = 0; i < 20; i++) (void)v4_fabric_step(&f);
|
|
CHECK(ND(a)->es.anticlock == ca && ND(b)->es.anticlock == cb, "a node that is waiting executes nothing, however long");
|
|
}
|
|
|
|
/* ---- an empty node is filled through its port, and runs what it was sent ---- */
|
|
{
|
|
static v4_node src;
|
|
static v4_cell prog[64], sent[96];
|
|
static stream s;
|
|
static v4_device dev;
|
|
unsigned len, count;
|
|
|
|
v4_node_reset(&src); /* the programme, assembled somewhere to be sent */
|
|
v4_asm_begin(&as, &src, ORG);
|
|
LIT(77); LIT(OUT); O(BANG_A); O(STORE_A);
|
|
LIT(PB + 1); O(BANG_B); LIT(99); O(STORE_B);
|
|
wait_for_ever();
|
|
count = (unsigned)(v4_asm_label(&as) - ORG);
|
|
CHECK(v4_asm_ok(&as) && count < 64, "a programme to send: %u words", count);
|
|
for (i = 0; i < count; i++) prog[i] = src.mem[ORG + i];
|
|
len = capsule(sent, prog, count, ORG);
|
|
|
|
/* sent by a device */
|
|
v4_fabric_init(&f, places, 4);
|
|
memset(&s, 0, sizeof s);
|
|
s.words = sent; s.count = len;
|
|
dev.give = stream_give; dev.take = stream_take; dev.self = &s;
|
|
b = (unsigned)v4_fabric_add(&f, &pool[2], PB);
|
|
CHECK(v4_fabric_wire_device(&f, b, 3, &dev), "a device is wired to port 3 of a node newly born");
|
|
steps = settle(5000);
|
|
CHECK(s.at == len, "the node took every word it was sent: %u of %u", s.at, len);
|
|
for (i = 0; i < count && ND(b)->n.mem[ORG + i] == prog[i]; i++) { }
|
|
CHECK(i == count, "the programme is in its memory, where it was sent to");
|
|
CHECK(ND(b)->n.mem[OUT] == 77, "and it ran: it left its mark");
|
|
CHECK(ND(b)->n.asking && ND(b)->n.ask_port == 1 && ND(b)->n.request == 99, "and is now writing to its port 1, which nothing is wired to");
|
|
CHECK(v4_fabric_wire_device(&f, b, 1, &dev) && settle(100) < 100 && s.took_n == 1 && s.took[0] == 99,
|
|
"a device wired there takes what it wrote");
|
|
|
|
/* sent by another node, which holds it in its own memory */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0);
|
|
for (i = 0; i < len; i++) ND(a)->n.mem[200 + i] = sent[i];
|
|
LIT(200); O(BANG_A); LIT(len - 1); O(PUSH); LIT(PB + 4); O(BANG_B);
|
|
(void)v4_asm_label(&as);
|
|
O(FETCH_INC); O(STORE_B); O(UNEXT); /* @+ !b unext: each write blocks in the middle of the word */
|
|
wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "a node that sends what is in its memory");
|
|
b = (unsigned)v4_fabric_add(&f, &pool[2], PB);
|
|
CHECK(v4_fabric_wire(&f, a, 4, b, 0), "is wired to a node newly born");
|
|
steps = settle(5000);
|
|
CHECK(ND(b)->n.mem[OUT] == 77 && ND(b)->n.asking && ND(b)->n.request == 99, "which takes it in and runs it, as from the device");
|
|
CHECK(ND(b)->n.last_from == 0, "it was listening to any port, and knows which it heard from: %u", ND(b)->n.last_from);
|
|
}
|
|
|
|
/* ---- a capsule of F18 code: any node's memory, sent to an empty node (capsule.h) ---- */
|
|
{
|
|
static v4_node src;
|
|
static v4_cell sent[400];
|
|
static stream s;
|
|
static v4_device dev;
|
|
unsigned k;
|
|
|
|
v4_node_reset(&src);
|
|
v4_asm_begin(&as, &src, 40); /* a programme, not at the start of memory */
|
|
LIT(123); LIT(OUT); O(BANG_A); O(STORE_A);
|
|
wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "a programme for the capsule");
|
|
src.mem[3] = 5; /* and words here and there: the first of memory, */
|
|
src.mem[200] = -1; src.mem[202] = 7; /* two with one zero between them, */
|
|
src.mem[300] = 9; src.mem[310] = 11; /* two with many, */
|
|
src.mem[PB - 1] = 13; /* and the last before the ports */
|
|
|
|
memset(&s, 0, sizeof s);
|
|
s.words = sent;
|
|
dev.give = stream_give; dev.take = stream_take; dev.self = &s;
|
|
s.count = 0;
|
|
s.count = v4_capsule_write(&src, 40, collect, &s);
|
|
CHECK(s.count > 0 && s.count < 400, "the capsule is %u words for a memory of %u", s.count, (unsigned)V4_NODE_WORDS);
|
|
s.at = 0;
|
|
|
|
v4_fabric_init(&f, places, 4);
|
|
b = (unsigned)v4_fabric_add(&f, &pool[0], PB);
|
|
CHECK(v4_fabric_wire_device(&f, b, 5, &dev), "it is sent to a node newly born");
|
|
steps = settle(5000);
|
|
CHECK(s.at == s.count, "which takes all of it: %u of %u", s.at, s.count);
|
|
for (k = 0; k < V4_NODE_WORDS && ND(b)->n.mem[k] == (k == (unsigned)OUT ? 123 : src.mem[k]); k++) { }
|
|
CHECK(k == V4_NODE_WORDS, "its memory is then the same, word for word (first difference at %u)", k);
|
|
CHECK(ND(b)->n.mem[OUT] == 123 && ND(b)->n.reading, "and it was started where the capsule says, and ran");
|
|
}
|
|
|
|
/* ---- a word is passed on by a node in between ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0);
|
|
LIT(PB + 0); O(BANG_B); LIT(7); O(STORE_B); LIT(8); O(STORE_B); LIT(9); O(STORE_B);
|
|
wait_for_ever();
|
|
b = loaded(1); /* whatever comes on any port goes out on port 6 */
|
|
LIT(ANY); O(BANG_A); LIT(PB + 6); O(BANG_B);
|
|
{
|
|
v4_cell again = v4_asm_label(&as);
|
|
O(FETCH_A); O(STORE_B);
|
|
v4_asm_branch(&as, V4_OP_JUMP, again);
|
|
}
|
|
c = loaded(2);
|
|
LIT(PB + 1); O(BANG_B); LIT(OUT); O(BANG_A); O(FETCH_B); O(STORE_INC); O(FETCH_B); O(STORE_INC); O(FETCH_B); O(STORE_INC);
|
|
wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "three nodes in a row assemble");
|
|
CHECK(v4_fabric_wire(&f, a, 0, b, 2) && v4_fabric_wire(&f, b, 6, c, 1), "the first is wired to the second, the second to the third");
|
|
steps = settle(2000);
|
|
CHECK(ND(c)->n.mem[OUT] == 7 && ND(c)->n.mem[OUT + 1] == 8 && ND(c)->n.mem[OUT + 2] == 9,
|
|
"what the first wrote reached the third, in order, through the one in between");
|
|
CHECK(ND(b)->n.last_from == 2, "the one in between heard it on its port 2");
|
|
|
|
/* ---- the wiring is changed while they run ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0); /* writes 1 2 3 4 ... to its port 0 for ever */
|
|
LIT(PB + 0); O(BANG_B); LIT(0);
|
|
{
|
|
v4_cell again = v4_asm_label(&as);
|
|
LIT(1); O(ADD); O(DUP); O(STORE_B);
|
|
v4_asm_branch(&as, V4_OP_JUMP, again);
|
|
}
|
|
b = loaded(1); LIT(PB + 3); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever();
|
|
c = loaded(2); LIT(PB + 3); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever();
|
|
CHECK(v4_asm_ok(&as), "a writer and two readers assemble");
|
|
CHECK(v4_fabric_wire(&f, a, 0, b, 3), "the writer is wired to the first reader");
|
|
(void)settle(500);
|
|
CHECK(ND(b)->n.mem[OUT] == 1 && ND(c)->n.mem[OUT] == 0, "who gets its first word");
|
|
CHECK(v4_fabric_wire(&f, a, 0, c, 3), "the same port is wired to the second reader instead");
|
|
(void)settle(500);
|
|
CHECK(ND(c)->n.mem[OUT] == 2, "who gets its next: %ld", (long)ND(c)->n.mem[OUT]);
|
|
CHECK(ND(a)->n.asking && ND(a)->n.request == 3, "and the writer waits with its third");
|
|
CHECK(v4_fabric_unwire(&f, a, 0) && settle(100) == 0 && ND(a)->n.asking, "with the wire cut it goes on waiting");
|
|
|
|
/* ---- asleep, awake, and removed, while looping; and a node born while others run ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0); /* counts in OUT for ever */
|
|
LIT(OUT); O(BANG_A);
|
|
{
|
|
v4_cell again = v4_asm_label(&as);
|
|
O(FETCH_A); LIT(1); O(ADD); O(STORE_A);
|
|
v4_asm_branch(&as, V4_OP_JUMP, again);
|
|
}
|
|
b = loaded(1); /* and so does this one */
|
|
LIT(OUT); O(BANG_A);
|
|
{
|
|
v4_cell again = v4_asm_label(&as);
|
|
O(FETCH_A); LIT(1); O(ADD); O(STORE_A);
|
|
v4_asm_branch(&as, V4_OP_JUMP, again);
|
|
}
|
|
CHECK(v4_asm_ok(&as), "two nodes that never stop assemble");
|
|
for (i = 0; i < 100; i++) (void)v4_fabric_step(&f);
|
|
{
|
|
v4_cell ca = ND(a)->n.mem[OUT], cb = ND(b)->n.mem[OUT];
|
|
v4_uheat_t clock = ND(a)->es.anticlock;
|
|
CHECK(ca > 0 && ca == cb, "each has had as much time as the other: %ld and %ld", (long)ca, (long)cb);
|
|
v4_fabric_sleep(&f, a);
|
|
for (i = 0; i < 100; i++) (void)v4_fabric_step(&f);
|
|
CHECK(ND(a)->n.mem[OUT] == ca && ND(a)->es.anticlock == clock, "put to sleep in the middle of its loop, one executes nothing");
|
|
CHECK(ND(b)->n.mem[OUT] == 2 * cb, "and the other goes on as before: %ld", (long)ND(b)->n.mem[OUT]);
|
|
v4_fabric_wake(&f, a);
|
|
for (i = 0; i < 100; i++) (void)v4_fabric_step(&f);
|
|
CHECK(ND(a)->n.mem[OUT] == 2 * ca, "woken, it goes on from where it was: %ld", (long)ND(a)->n.mem[OUT]);
|
|
CHECK(v4_fabric_remove(&f, a) != 0 && v4_fabric_node_at(&f, a) == 0, "it is removed while it loops");
|
|
cb = ND(b)->n.mem[OUT];
|
|
for (i = 0; i < 100; i++) (void)v4_fabric_step(&f);
|
|
CHECK(ND(b)->n.mem[OUT] > cb, "and the other goes on");
|
|
c = (unsigned)v4_fabric_add(&f, &pool[3], PB);
|
|
CHECK(c == a && pool[3].n.p == ANY, "a node is born while the other runs, into the place that was freed");
|
|
for (i = 0; i < 10; i++) (void)v4_fabric_step(&f);
|
|
CHECK(pool[3].n.reading && pool[3].es.anticlock == 0, "and waits, empty, while the other runs");
|
|
}
|
|
|
|
/* ---- more room, while they run ---- */
|
|
v4_fabric_init(&f, places, 4);
|
|
a = loaded(0); LIT(PB + 0); O(BANG_B); LIT(5); O(STORE_B); wait_for_ever();
|
|
b = loaded(1); LIT(PB + 0); O(BANG_B); O(FETCH_B); LIT(OUT); O(BANG_A); O(STORE_A); wait_for_ever();
|
|
(void)loaded(2); wait_for_ever();
|
|
(void)loaded(3); wait_for_ever();
|
|
CHECK(v4_fabric_add(&f, &pool[4], PB) == -1, "a fabric with no room left takes no more nodes");
|
|
CHECK(v4_fabric_wire(&f, a, 0, b, 0), "two of its nodes are wired");
|
|
v4_fabric_grow(&f, more_places, NODES);
|
|
CHECK(v4_fabric_add(&f, &pool[4], PB) == 4, "given more room, it takes another");
|
|
(void)settle(500);
|
|
CHECK(ND(b)->n.mem[OUT] == 5, "and the nodes that were there are wired as they were");
|
|
|
|
/* ---- what cannot be wired ---- */
|
|
CHECK(!v4_fabric_wire(&f, a, V4_PORTS, b, 0) && !v4_fabric_wire(&f, a, 0, 7, 0) && !v4_fabric_wire(&f, a, 1, a, 1),
|
|
"a port that is not there, a node that is not there, a port to itself: refused");
|
|
CHECK(v4_fabric_wire(&f, a, 1, a, 2), "but one port of a node may be wired to another of the same node");
|
|
|
|
printf(" %d checks, %d failures\n", checks, failures);
|
|
return failures != 0;
|
|
}
|