diff --git a/docs/v4.0.0/DATASHEET-F18.md b/docs/v4.0.0/DATASHEET-F18.md index 06c22657..44ff8a43 100644 --- a/docs/v4.0.0/DATASHEET-F18.md +++ b/docs/v4.0.0/DATASHEET-F18.md @@ -98,7 +98,7 @@ registers (§10.3). the console receive addresses, where a store goes to RAM. - **ROM.** Designed, not built: `MACHINE.md` §2, and plan Task 1. It is a read-only range at `V4_NODE_WORDS` and above. It can be fetched and executed. A store there is an address fault. - A reset keeps it. + In the model it is attached by the owner, as every device is, and a reset detaches it. - Its routines are shared code, GA144-style (`MACHINE.md` 2.1). Payloads call them through the entry table at the ROM's base. Their state lives in a reserved RAM area. This sheet will list each entry's stack effect and the registers it uses once the ROM is built. diff --git a/v4/Makefile b/v4/Makefile index ac7d0edf..0b9cbf82 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -47,7 +47,11 @@ BINDIR := $(HERE)/build HOST_WORDS := 16384 HOST_DATA_RING := 30 HOST_RET_RING := 31 -node_size = $(if $(findstring /test_host_,$(1)),-DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING)) +# The host node's ROM range (docs/v4.0.0/MACHINE.md section 2): words above +# RAM that the machine can fetch and run but never write. Its size is +# whatever the ROM takes; this is the range it may use. +HOST_ROM_WORDS := 4096 +node_size = $(if $(findstring /test_host_,$(1)),-DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) -DV4_ROM_WORDS=$(HOST_ROM_WORDS)) # BLOCKS (docs/v4.0.0/MESH.md section 8). A node asks its kernel for a # block, and system/blocks.c serves it from the kernel's block subsystem, @@ -97,7 +101,7 @@ capsule_dir := -DV4_CAPSULE_DIR='"$(HERE)/capsule"' ROOT := $(abspath $(HERE)/..) NUCLEUS_64 := $(ROOT)/capsules/v4/nucleus-64.f18 NUCLEUS_32 := $(BINDIR)/nucleus-32.f18 -HOST_DEFS := -DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) +HOST_DEFS := -DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) -DV4_ROM_WORDS=$(HOST_ROM_WORDS) ENGINE_SRCS := $(addprefix $(HERE)/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c wire.c) define IMAGE_RULE diff --git a/v4/include/v4/node.h b/v4/include/v4/node.h index 664f7433..94202ff9 100644 --- a/v4/include/v4/node.h +++ b/v4/include/v4/node.h @@ -57,6 +57,18 @@ #define V4_NODE_WORDS 1024u #endif +/* THE ROM (docs/v4.0.0/MACHINE.md section 2, DATASHEET-F18.md section 5). + * A read-only range of V4_ROM_WORDS words at V4_ROM_BASE, just above RAM. + * An instruction fetch, an @p literal and a data fetch there read the ROM; a + * running programme's store there is an address fault. In the model it is + * attached, as every device is, and v4_node_reset detaches it: the owner + * attaches it again after a reset. The range's size is a build parameter, + * as RAM's is; 0 (the default) is a machine with no ROM. */ +#ifndef V4_ROM_WORDS +#define V4_ROM_WORDS 0u +#endif +#define V4_ROM_BASE ((v4_cell)V4_NODE_WORDS) + /* 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 @@ -83,6 +95,11 @@ typedef struct { v4_cell mem[V4_NODE_WORDS]; v4_cell mem_guard_tail[V4_MEM_BOUND]; + /* The ROM: its words, and how many there are (0: none). See + * v4_node_rom_attach. */ + const v4_cell *rom; + unsigned rom_words; + /* CONSOLE-TX capture (DECOMPOSITION.md section 7). See * v4_node_console_attach below. */ v4_cell console_tx; /* its word address, or -1: no console */ @@ -334,6 +351,14 @@ v4_cell v4_node_fetch(v4_node *n, v4_cell addr); /* 1 if `addr` is a word of node memory. */ int v4_node_addr_ok(v4_cell addr); +/* 1 if `addr` can be fetched from: a word of node memory, or of the ROM. */ +int v4_node_addr_readable(const v4_node *n, v4_cell addr); + +/* Give the machine its ROM: `words` words at V4_ROM_BASE, read from `rom`, + * which must outlive the node. More than V4_ROM_WORDS words, or a null + * `rom`, attaches none. v4_node_reset detaches it. */ +void v4_node_rom_attach(v4_node *n, const v4_cell *rom, unsigned words); + /* FAULTS (DECOMPOSITION.md D-14 and D-16, ruled 2026-10-04: an address * outside the node's memory, and a stack pushed when full or popped when * empty, are guarded; an error is shown; the node returns to its prompt). diff --git a/v4/src/exec.c b/v4/src/exec.c index 9757897f..ead41dd5 100644 --- a/v4/src/exec.c +++ b/v4/src/exec.c @@ -43,6 +43,14 @@ static int faulted(v4_node *n, v4_cell addr) return 1; } +/* The same for a fetch, which may also read the ROM (node.h). */ +static int faulted_read(v4_node *n, v4_cell addr) +{ + if (v4_node_addr_readable(n, addr)) return 0; + v4_node_fault(n, V4_FAULT_ADDRESS, addr); + return 1; +} + /* D-16: what each opcode takes from the stacks and leaves on them. If the * data stack holds fewer than `dneed`, or the return stack fewer than * `rneed`, or a stack the opcode leaves one more on is full, it is a fault @@ -142,7 +150,7 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h, break; case V4_OP_FETCH_P: - if (faulted(n, n->p)) return; + if (faulted_read(n, n->p)) return; if (v4_node_port_index(n, n->p) >= 0) { /* P is a port: the literal is the next word its neighbour * writes, and P stays where it is */ @@ -154,18 +162,18 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h, n->p = add_wrap(n->p, 1); break; case V4_OP_FETCH_INC: - if (faulted(n, n->a)) return; + if (faulted_read(n, n->a)) return; if (!v4_node_read_ready(n, n->a)) return; /* a port with nothing written to it yet */ v4_dstack_push(ds, v4_node_fetch(n, n->a)); n->a = add_wrap(n->a, 1); break; case V4_OP_FETCH_B: - if (faulted(n, n->b)) return; + if (faulted_read(n, n->b)) return; if (!v4_node_read_ready(n, n->b)) return; /* a port with nothing written to it yet */ v4_dstack_push(ds, v4_node_fetch(n, n->b)); break; case V4_OP_FETCH_A: - if (faulted(n, n->a)) return; + if (faulted_read(n, n->a)) return; if (!v4_node_read_ready(n, n->a)) return; /* a port with nothing written to it yet */ v4_dstack_push(ds, v4_node_fetch(n, n->a)); break; @@ -269,7 +277,7 @@ unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h) slot = es->resume_slot; } else { /* P itself may be what is outside memory */ - if (faulted(n, n->p)) return 0; + if (faulted_read(n, n->p)) return 0; if (v4_node_port_index(n, n->p) >= 0) { /* P is a port: the node executes what its neighbour writes * there, and P stays where it is */ diff --git a/v4/src/node.c b/v4/src/node.c index 1fda6759..7ae64984 100644 --- a/v4/src/node.c +++ b/v4/src/node.c @@ -4,6 +4,8 @@ void v4_node_reset(v4_node *n) { + n->rom = NULL; /* attached again by the owner, as every device is */ + n->rom_words = 0; n->p = 0; n->a = 0; n->b = 0; @@ -66,10 +68,24 @@ int v4_node_guards_intact(const v4_node *n) && v4_rstack_guards_intact(&n->rs); } +int v4_node_addr_readable(const v4_node *n, v4_cell addr) +{ + return v4_node_addr_ok(addr) + || ((v4_ucell)addr - (v4_ucell)V4_ROM_BASE) < (v4_ucell)n->rom_words; +} + +void v4_node_rom_attach(v4_node *n, const v4_cell *rom, unsigned words) +{ + if (!rom || words > V4_ROM_WORDS) { n->rom = NULL; n->rom_words = 0; return; } + n->rom = rom; + n->rom_words = words; +} + v4_cell v4_node_load(const v4_node *n, v4_cell addr) { - if (!v4_node_addr_ok(addr)) return 0; - return n->mem[(unsigned)addr]; + if (v4_node_addr_ok(addr)) return n->mem[(unsigned)addr]; + if (v4_node_addr_readable(n, addr)) return n->rom[(v4_ucell)addr - (v4_ucell)V4_ROM_BASE]; + return 0; } void v4_node_store(v4_node *n, v4_cell addr, v4_cell value) diff --git a/v4/src/text.c b/v4/src/text.c index 6da0ffdf..73c15cdb 100644 --- a/v4/src/text.c +++ b/v4/src/text.c @@ -216,7 +216,7 @@ static void place_branch(v4_text *t) v4_asm *as = &t->as; if (as->slot == 0) return; if (!v4_iword_branch_legal(as->slot) - || (v4_ucell)v4_iword_slot_mask(as->slot) < (v4_ucell)(V4_NODE_WORDS - 1u)) + || (v4_ucell)v4_iword_slot_mask(as->slot) < (v4_ucell)(V4_NODE_WORDS + V4_ROM_WORDS - 1u)) (void)v4_asm_label(as); } diff --git a/v4/tests/test_host_rom_range.c b/v4/tests/test_host_rom_range.c new file mode 100644 index 00000000..070e3bb2 --- /dev/null +++ b/v4/tests/test_host_rom_range.c @@ -0,0 +1,101 @@ +/* test_host_rom_range.c -- the ROM is the machine's: read and run, never written. + * + * docs/v4.0.0/MACHINE.md section 2, DATASHEET-F18.md section 5. The ROM is a + * read-only range at V4_ROM_BASE, above RAM. An instruction fetch, an @p + * literal and a data fetch there read the ROM; a running programme's store + * there is an address fault. Like every device in the model it is attached + * by the node's owner, and a reset detaches it. + */ +#include "v4/asm.h" +#include "v4/testcode.h" +#include + +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, scratch; +static v4_exec_state es; +static v4_heat h; +static v4_cell rom[16]; + +/* ROM words 0..: "7 ;" at ROM+0, then a cell holding 42 at ROM+8 */ +static void build_rom(void) +{ + v4_asm ra; + unsigned i; + v4_node_reset(&scratch); + v4_asm_begin(&ra, &scratch, 0); + v4_asm_lit(&ra, 7); + v4_asm_op(&ra, V4_OP_SEMI); + CHECK(v4_asm_ok(&ra), "the ROM's code assembles"); + for (i = 0; i < 16; i++) rom[i] = scratch.mem[i]; + rom[8] = 42; +} + +int main(void) +{ + v4_asm as; + long r; + + build_rom(); + v4_node_reset(&n); + v4_exec_reset(&es); + v4_heat_reset(&h); + CHECK(V4_ROM_BASE == (v4_cell)V4_NODE_WORDS, "the ROM starts where RAM ends"); + CHECK(v4_node_load(&n, V4_ROM_BASE) == 0, "no ROM attached: nothing there"); + v4_node_rom_attach(&n, rom, 16); + CHECK(v4_node_load(&n, V4_ROM_BASE) == rom[0], "the ROM reads"); + CHECK(v4_node_load(&n, V4_ROM_BASE + 16) == 0, "past the ROM's words: nothing"); + + /* RAM 16: call the ROM's word, then return */ + v4_asm_begin(&as, &n, 16); + v4_asm_branch(&as, V4_OP_CALL, V4_ROM_BASE); + v4_asm_op(&as, V4_OP_SEMI); + CHECK(v4_asm_ok(&as), "a call into the ROM assembles"); + r = v4_test_call(&n, &es, &h, 16, 100); + CHECK(r > 0, "the call returns"); + CHECK(n.ds.depth == 1 && n.ds.t == 7, "code in the ROM runs: depth %u T %ld", n.ds.depth, (long)n.ds.t); + + /* RAM 32: fetch the ROM's cell 8 through A */ + v4_dstack_reset(&n.ds); + v4_asm_begin(&as, &n, 32); + v4_asm_lit(&as, V4_ROM_BASE + 8); + v4_asm_op(&as, V4_OP_BANG_A); + v4_asm_op(&as, V4_OP_FETCH_A); + v4_asm_op(&as, V4_OP_SEMI); + CHECK(v4_asm_ok(&as), "a fetch from the ROM assembles"); + r = v4_test_call(&n, &es, &h, 32, 100); + CHECK(r > 0 && n.ds.depth == 1 && n.ds.t == 42, "a data fetch reads the ROM: T %ld", (long)n.ds.t); + + /* RAM 48: store 99 into the ROM's cell 8 through B */ + v4_dstack_reset(&n.ds); + v4_asm_begin(&as, &n, 48); + v4_asm_lit(&as, V4_ROM_BASE + 8); + v4_asm_op(&as, V4_OP_BANG_B); + v4_asm_lit(&as, 99); + v4_asm_op(&as, V4_OP_STORE_B); + v4_asm_op(&as, V4_OP_SEMI); + CHECK(v4_asm_ok(&as), "a store into the ROM assembles"); + r = v4_test_call(&n, &es, &h, 48, 100); + CHECK(r < 0, "a store into the ROM does not return"); + CHECK(n.fault_kind == V4_FAULT_ADDRESS && n.fault_addr == V4_ROM_BASE + 8, + "it is an address fault at the ROM's address: kind %u addr %ld", n.fault_kind, (long)n.fault_addr); + CHECK(v4_node_load(&n, V4_ROM_BASE + 8) == 42, "and the ROM is unchanged"); + + /* C code's store does nothing there either */ + v4_node_store(&n, V4_ROM_BASE + 8, 99); + CHECK(v4_node_load(&n, V4_ROM_BASE + 8) == 42, "v4_node_store leaves the ROM alone"); + + /* a reset detaches it, as it does every device */ + v4_node_reset(&n); + CHECK(v4_node_load(&n, V4_ROM_BASE) == 0, "a reset detaches the ROM"); + v4_node_rom_attach(&n, rom, 16); + CHECK(v4_node_load(&n, V4_ROM_BASE) == rom[0], "and the owner attaches it again"); + + /* more words than the range holds are refused: nothing attached */ + v4_node_rom_attach(&n, rom, V4_ROM_WORDS + 1u); + CHECK(v4_node_load(&n, V4_ROM_BASE) == 0, "a ROM larger than the range is not attached"); + + printf("test_host_rom_range: %d checks, %d failures\n", checks, failures); + return failures != 0; +}