diff --git a/Kconfig.kernel b/Kconfig.kernel
index 9cf88534..25c5017a 100644
--- a/Kconfig.kernel
+++ b/Kconfig.kernel
@@ -13,6 +13,17 @@ config STARFORTH_ENABLE_VM
no capsules, no "ok" REPL. Gates a large source-file selection
block in kernel/Makefile, not just a handful of -D flags.
+config STARFORTH_V4
+ bool "Boot StarForth v4, the F18-derived engine, at a single prompt (STARFORTH_V4)"
+ default n
+ help
+ Instead of the v3 VM and its fleet, the kernel starts one StarForth
+ v4 host node after the M0-M6 hardware milestones: the golden model
+ of the 32-opcode engine (v4/src) running the capsule image built
+ from v4/capsule, with its console on the kernel's serial console.
+ It reaches v4's "ok> " prompt and stays there. The v3 VM is still
+ compiled in but is not started. See docs/v4.0.0/DECOMPOSITION.md.
+
config PARITY_MODE
bool "Deterministic parity harness mode (PARITY_MODE)"
default n
diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md
index d2477242..2ab3a33f 100644
--- a/capsules/BLOCK_MAP.md
+++ b/capsules/BLOCK_MAP.md
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
-
+
@@ -29,6 +29,7 @@
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | n/a |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | n/a |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | n/a |
+| `v4:forth79.4th` | 6000 | `0xa4b74bdc7deaf204` | n/a |
| `workload-0.4th` | 2200, 2201 | `0x93f86f60aeba8feb` | n/a |
| `workload-1-lite.4th` | 5058, 5059 | `0x44a7a7e3176dcc8d` | n/a |
| `workload-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | n/a |
@@ -366,10 +367,11 @@
| 5113 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5114 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5115 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
+| 6000 | `v4:forth79.4th` | `0xa4b74bdc7deaf204` | ok |
## Conflicts
None.
---
-*36 capsule(s) scanned. Re-run `mkcapsule --manifest
` to refresh.*
+*37 capsule(s) scanned. Re-run `mkcapsule --manifest ` to refresh.*
diff --git a/capsules/v4/forth79.4th b/capsules/v4/forth79.4th
new file mode 100644
index 00000000..47be6dff
--- /dev/null
+++ b/capsules/v4/forth79.4th
@@ -0,0 +1,6 @@
+Block 6000
+( forth79.4th -- the FORTH-79 Required Word Set for v4, )
+( as colon definitions, loaded when the system boots. )
+( docs/v4.0.0/NUCLEUS.md. Blocks 6000 up. )
+( A word moves here from the assembled nucleus, v4/capsule, )
+( once its colon definition passes POST. None has moved yet. )
diff --git a/kernel/Makefile b/kernel/Makefile
index 2ea15960..17416ec2 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -60,6 +60,9 @@ $(eval $(call kconfig_bool,PARITY_MODE,0))
# StarForth VM integration (default: enabled)
$(eval $(call kconfig_bool,STARFORTH_ENABLE_VM,1))
+# StarForth v4 at a single prompt in place of the v3 VM (default: off)
+$(eval $(call kconfig_bool,STARFORTH_V4,0))
+
# Monolithic build — loader + kernel compiled together (default: enabled)
MONOLITHIC ?= 1
@@ -597,6 +600,30 @@ LOADER_VM_OBJS := $(patsubst v3/src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_S
KERNEL_VM_OBJS := $(patsubst v3/src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
endif
+# ------------------------------------------------------------------------------
+# StarForth v4 (STARFORTH_V4=1): the golden model of the F18-derived engine,
+# v4/src, and the nucleus image v4's own Makefile builds on this machine from
+# v4/capsule (docs/v4.0.0/NUCLEUS.md). The node is the host node's size, with 64-bit cells, as every
+# ISA this kernel boots on has (DECOMPOSITION.md D-5), so the image is the
+# same file for all three.
+# ------------------------------------------------------------------------------
+ifeq ($(STARFORTH_V4),1)
+V4_DEFS := -DSTARFORTH_V4=1 -Iv4/include \
+ -DV4_CELL_BITS=64 -DV4_NODE_WORDS=16384 -DV4_DATA_RING=30 -DV4_RET_RING=31
+KERNEL_CFLAGS += $(V4_DEFS)
+LOADER_CFLAGS += $(V4_DEFS)
+
+V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c)
+V4_IMAGE_C := $(BUILD_DIR)/v4_image_64.c
+
+LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
+KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
+# v4/system/boot.c is the boot the hosted v4 system runs too: nucleus, then
+# the capsules from this kernel's own capsule directory, then the prompt.
+LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_image.o $(LOADER_OBJ_DIR)/v4system/boot.o
+KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_image.o $(KERNEL_OBJ_DIR)/v4system/boot.o
+endif
+
LOADER_SRCS := $(LOADER_SRCS_BASE) $(LOADER_EXTRA_SRCS)
KERNEL_SRCS := $(KERNEL_SRCS_BASE) $(KERNEL_EXTRA_SRCS)
@@ -605,12 +632,14 @@ LOADER_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ARCH_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ASM)) \
$(LOADER_VM_OBJS) \
+ $(LOADER_V4_OBJS) \
$(CAPSULE_GENERATED_OBJ)
KERNEL_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_ASM)) \
$(KERNEL_VM_OBJS) \
+ $(KERNEL_V4_OBJS) \
$(CAPSULE_GENERATED_KOBJ)
# ==============================================================================
@@ -733,6 +762,41 @@ else
@$(LOADER_CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(LOADER_CFLAGS)) -c $< -o $@
endif
+# StarForth v4: the capsule image, built on this machine by v4's Makefile, and
+# the engine, compiled like any other kernel source.
+ifeq ($(STARFORTH_V4),1)
+$(V4_IMAGE_C): FORCE
+ @mkdir -p $(dir $@)
+ @echo " V4IMG v4/capsule -> $@"
+ @$(MAKE) --no-print-directory -C v4 CC=cc build/v4_image_64.c
+ @cmp -s v4/build/v4_image_64.c $@ || cp v4/build/v4_image_64.c $@
+
+$(LOADER_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(LOADER_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (loader) $<"
+ @$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
+$(KERNEL_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(KERNEL_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (kernel) $<"
+ @$(CC) $(KERNEL_CFLAGS) -c $< -o $@
+$(LOADER_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(LOADER_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (loader) $<"
+ @$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
+$(KERNEL_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(KERNEL_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (kernel) $<"
+ @$(CC) $(KERNEL_CFLAGS) -c $< -o $@
+$(LOADER_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(LOADER_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (loader) $<"
+ @$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
+$(KERNEL_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(KERNEL_OBJ_DIR)
+ @mkdir -p $(dir $@)
+ @echo "CC (kernel) $<"
+ @$(CC) $(KERNEL_CFLAGS) -c $< -o $@
+endif
+
# Compile loader assembly sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@@ -1337,6 +1401,7 @@ info:
@echo "Loader output: $(LOADER_EFI)"
@echo "Kernel output: $(KERNEL_ELF)"
@echo "VM integration: $(STARFORTH_ENABLE_VM)"
+ @echo "StarForth v4: $(STARFORTH_V4)"
@echo "Monolithic: $(MONOLITHIC)"
help:
diff --git a/kernel/include/starkernel/capsule_blocks.h b/kernel/include/starkernel/capsule_blocks.h
new file mode 100644
index 00000000..b3b5645d
--- /dev/null
+++ b/kernel/include/starkernel/capsule_blocks.h
@@ -0,0 +1,21 @@
+/* capsule_blocks.h -- the block format of a .4th capsule payload.
+ *
+ * A .4th capsule is text: "Block " header lines, each followed by that
+ * block's content lines (tools/mkcapsule.c, validate_forth_blocks). This is
+ * the one place that says what a header line is. It depends on no VM, so
+ * every loader of capsules can use it: the kernel's and the hosted v4
+ * system's (docs/v4.0.0/NUCLEUS.md 5.3).
+ */
+#ifndef STARKERNEL_CAPSULE_BLOCKS_H
+#define STARKERNEL_CAPSULE_BLOCKS_H
+
+#include
+
+/* Is the line that starts at p a "Block " header? If so, returns 1
+ * with the number in *out_num and the start of the next line in *out_after;
+ * otherwise returns 0 and changes nothing. `end` is one past the payload's
+ * last byte. */
+int capsule_block_header(const uint8_t *p, const uint8_t *end,
+ uint32_t *out_num, const uint8_t **out_after);
+
+#endif /* STARKERNEL_CAPSULE_BLOCKS_H */
diff --git a/kernel/include/starkernel/v4/sk_v4.h b/kernel/include/starkernel/v4/sk_v4.h
new file mode 100644
index 00000000..26697cd2
--- /dev/null
+++ b/kernel/include/starkernel/v4/sk_v4.h
@@ -0,0 +1,13 @@
+/* sk_v4.h -- StarForth v4 on bare metal: one host node at a prompt.
+ *
+ * Built only with STARFORTH_V4=1 (Kconfig.kernel).
+ */
+#ifndef STARKERNEL_V4_SK_V4_H
+#define STARKERNEL_V4_SK_V4_H
+
+/* Start one StarForth v4 host node from the capsule image linked into the
+ * kernel, with its console on the kernel's console, and run it. Does not
+ * return. */
+void sk_v4_run(void);
+
+#endif /* STARKERNEL_V4_SK_V4_H */
diff --git a/kernel/src/capsule/capsule_blocks.c b/kernel/src/capsule/capsule_blocks.c
new file mode 100644
index 00000000..2a8c2448
--- /dev/null
+++ b/kernel/src/capsule/capsule_blocks.c
@@ -0,0 +1,29 @@
+/* capsule_blocks.c -- the block format of a .4th capsule payload.
+ * See capsule_blocks.h. Nothing here uses the C library or a VM.
+ */
+#include "starkernel/capsule_blocks.h"
+
+int capsule_block_header(const uint8_t *p, const uint8_t *end,
+ uint32_t *out_num, const uint8_t **out_after)
+{
+ const uint8_t *q;
+ uint32_t num = 0;
+
+ if ((uint64_t)(end - p) < 7u) return 0;
+ if (p[0] != 'B' || p[1] != 'l' || p[2] != 'o' || p[3] != 'c' || p[4] != 'k' || p[5] != ' ')
+ return 0;
+
+ q = p + 6;
+ if (q >= end || *q < '0' || *q > '9') return 0;
+ while (q < end && *q >= '0' && *q <= '9') {
+ num = num * 10u + (uint32_t)(*q - '0');
+ q++;
+ }
+
+ while (q < end && *q != '\n') q++;
+ if (q < end) q++;
+
+ *out_num = num;
+ *out_after = q;
+ return 1;
+}
diff --git a/kernel/src/kernel_main.c b/kernel/src/kernel_main.c
index 4a4329e2..fcd390a0 100644
--- a/kernel/src/kernel_main.c
+++ b/kernel/src/kernel_main.c
@@ -81,6 +81,9 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
log.h's line length (LOG_MSG_LINE_MAX, 256)
are distinct names */
#include "version.h"
+#ifdef STARFORTH_V4
+#include "starkernel/v4/sk_v4.h"
+#endif
#endif
/* Forward declaration — kernel_main_deep contains everything from heartbeat
@@ -512,6 +515,14 @@ static void kernel_main_deep(BootInfo *boot_info) {
console_println("Kernel initialization complete.");
console_println("Boot successful!\n");
+#ifdef STARFORTH_V4
+ /* StarForth v4 at a single prompt (Kconfig STARFORTH_V4): one host node
+ * of the F18-derived engine, in place of the v3 VM and everything below.
+ * It does not return. */
+ (void)boot_info;
+ sk_v4_run();
+#endif
+
#ifdef STARFORTH_ENABLE_VM
/* Stadium: boot-time allocation (FABRIC-0.md item 3.2), before any VM
* exists (§6). Soft failure -- nothing downstream consumes the Stadium
diff --git a/kernel/src/v4/sk_v4.c b/kernel/src/v4/sk_v4.c
new file mode 100644
index 00000000..61ff98df
--- /dev/null
+++ b/kernel/src/v4/sk_v4.c
@@ -0,0 +1,86 @@
+/* sk_v4.c -- StarForth v4 on bare metal: one host node at a prompt.
+ *
+ * The node is the golden model of the F18-derived engine (v4/src). It comes
+ * up as the hosted v4 system does, by v4_boot_run (v4/include/v4/boot.h):
+ * the nucleus image, then the capsules from the directory baked into this
+ * kernel, each checked and its parity line printed, then the prompt.
+ * docs/v4.0.0/NUCLEUS.md. This file is all the kernel adds:
+ *
+ * - what the node prints goes to the kernel's console;
+ * - what is typed is given to the node a line at a time. The node does
+ * not send back what it reads -- on the mesh that is the console node's
+ * job -- so the line is edited here: characters are echoed, backspace
+ * rubs one out, and Enter hands the line over;
+ * - blocks are kept in memory, and are gone at power-off.
+ *
+ * Nothing of the v3 VM is started. docs/v4.0.0/DECOMPOSITION.md.
+ */
+#include "starkernel/v4/sk_v4.h"
+#include "starkernel/console.h"
+#include "v4/boot.h"
+
+#define SK_V4_BLOCKS 64u
+#define SK_V4_LINE 79u /* QUERY takes 80 characters: 79 and the new-line */
+
+static v4_node sk_v4_node;
+static v4_exec_state sk_v4_es;
+static v4_heat sk_v4_heat;
+static unsigned char sk_v4_disk[SK_V4_BLOCKS * V4_BLOCK_BYTES];
+
+static char sk_v4_line[SK_V4_LINE + 1u];
+static unsigned sk_v4_len;
+
+/* One character from the keyboard. A finished line goes to the node. */
+static void sk_v4_key(int c)
+{
+ if (c == '\r' || c == '\n') {
+ console_putc('\n');
+ sk_v4_line[sk_v4_len++] = '\n';
+ (void)v4_node_console_feed(&sk_v4_node, sk_v4_line, sk_v4_len);
+ sk_v4_len = 0;
+ } else if (c == 8 || c == 127) {
+ if (sk_v4_len > 0) {
+ sk_v4_len--;
+ console_putc(8); console_putc(' '); console_putc(8);
+ }
+ } else if (c >= 32 && c < 127 && sk_v4_len < SK_V4_LINE) {
+ sk_v4_line[sk_v4_len++] = (char)c;
+ console_putc((char)c);
+ }
+}
+
+static void sk_v4_out(const char *text, unsigned len)
+{
+ unsigned i;
+ for (i = 0; i < len; i++) console_putc(text[i]);
+}
+
+void sk_v4_run(void)
+{
+ const v4_image *im = &v4_capsule_image;
+ v4_boot boot;
+ unsigned i;
+
+ console_println("StarForth v4: one host node, the F18-derived engine");
+ boot.n = &sk_v4_node; boot.es = &sk_v4_es; boot.h = &sk_v4_heat; boot.im = im; boot.out = sk_v4_out;
+ if (!v4_boot_run(&boot, sk_v4_disk, SK_V4_BLOCKS)) {
+ console_println("StarForth v4: not started");
+ for (;;) { }
+ }
+
+ for (;;) {
+ (void)v4_exec_step_word(&sk_v4_node, &sk_v4_es, &sk_v4_heat);
+ if (sk_v4_node.console_len) {
+ for (i = 0; i < sk_v4_node.console_len; i++) console_putc((char)sk_v4_node.console[i]);
+ sk_v4_node.console_len = 0;
+ }
+ if (sk_v4_node.stopped) {
+ console_println("StarForth v4: the node stopped on a fault");
+ for (;;) { }
+ }
+ if (v4_image_waiting(&sk_v4_node, im)) {
+ int c = console_getc();
+ if (c >= 0) sk_v4_key(c);
+ }
+ }
+}
diff --git a/v4/Makefile b/v4/Makefile
index 697b2bba..c5b983fa 100644
--- a/v4/Makefile
+++ b/v4/Makefile
@@ -55,6 +55,91 @@ node_size = $(if $(findstring /test_host_,$(1)),-DV4_NODE_WORDS=$(HOST_WORDS) -D
CAPSULES := $(wildcard $(HERE)/capsule/*.v4)
capsule_dir := -DV4_CAPSULE_DIR='"$(HERE)/capsule"'
+# THE NUCLEUS IMAGE. tools/mkimage.c, built for the host node (HOST_WORDS
+# and the host stack sizes) at one cell width, assembles the nucleus --
+# capsule/*.v4 -- and writes it as a C file, $(BINDIR)/v4_image_.c.
+# The hosted system below and the bare-metal kernel (kernel/Makefile,
+# STARFORTH_V4=1) link the 64-bit one. docs/v4.0.0/NUCLEUS.md.
+HOST_DEFS := -DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING)
+ENGINE_SRCS := $(addprefix $(HERE)/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c)
+
+define IMAGE_RULE
+$(BINDIR)/mkimage-$(1): $(HERE)/tools/mkimage.c $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/tests/host_map.h $(HERE)/Makefile
+ @mkdir -p $(BINDIR)
+ $$(CC) $$(CFLAGS) -I$(HERE)/include -DV4_CELL_BITS=$(1) $(HOST_DEFS) $(capsule_dir) $(HERE)/tools/mkimage.c $$(SRCS) -o $$@
+
+$(BINDIR)/v4_image_$(1).c: $(BINDIR)/mkimage-$(1) $$(CAPSULES)
+ $(BINDIR)/mkimage-$(1) $$@
+endef
+$(foreach w,$(WIDTHS),$(eval $(call IMAGE_RULE,$(w))))
+
+.PHONY: image
+image: $(foreach w,$(WIDTHS),$(BINDIR)/v4_image_$(w).c)
+
+# THE HOSTED SYSTEM: StarForth v4 as a Linux program, a product, for amd64,
+# aarch64 and riscv64 -- $(BINDIR)/starforth4-. It is the engine, the
+# 64-bit nucleus image, the capsule directory and the boot (system/boot.c)
+# that loads the capsules from it: the same four the kernel links. The
+# directory is made by the repository's own tools/mkcapsule.c from
+# ../capsules, signed if the key is on this machine (kernel/Makefile,
+# SIGN_KEY), and the code that reads it is the kernel's, compiled here as it
+# is there. The binaries are static, so the two foreign ones run under
+# user-mode QEMU with nothing else installed.
+ROOT := $(abspath $(HERE)/..)
+HOSTED_ISAS := amd64 aarch64 riscv64
+CC_amd64 ?= cc
+CC_aarch64 ?= aarch64-linux-gnu-gcc
+CC_riscv64 ?= riscv64-linux-gnu-gcc
+RUN_amd64 ?=
+RUN_aarch64 ?= qemu-aarch64
+RUN_riscv64 ?= qemu-riscv64
+
+SIGN_KEY ?= /home/rajames/CLionProjects/lithosananke-ca/intermediate/snakeoil-intermediate.key
+SIGN_KEY_ARGS = $(if $(wildcard $(SIGN_KEY)),--sign-key $(SIGN_KEY),)
+CRYPTO_SRCS := $(addprefix $(ROOT)/kernel/src/crypto/,ed25519.c fe25519.c scalar25519.c sha512.c)
+MKCAPSULE_SRCS := $(ROOT)/tools/mkcapsule.c $(ROOT)/tools/pkcs8_ed25519.c $(CRYPTO_SRCS)
+CAPSULE_FILES := $(shell find $(ROOT)/capsules -type f ! -name '.*' 2>/dev/null)
+CAPSULE_DIR_C := $(BINDIR)/capsule_generated.c
+
+# the kernel's capsule code: find, check the hash, verify the signature,
+# split a payload into blocks
+SYSTEM_SRCS := $(HERE)/system/boot.c \
+ $(addprefix $(ROOT)/kernel/src/capsule/,capsule_find.c capsule_validate.c capsule_sig.c capsule_blocks.c) \
+ $(ROOT)/kernel/src/hash/xxhash64.c $(ROOT)/kernel/src/crypto/x509_ed25519.c $(CRYPTO_SRCS)
+# The kernel's sources are held to the kernel's warnings, not this model's.
+SYSTEM_CFLAGS := $(CSTD) -Wall -Wextra $(OPT) -I$(HERE)/include -I$(ROOT)/kernel/include -I$(ROOT)/v3/include \
+ -DV4_CELL_BITS=64 $(HOST_DEFS)
+
+$(BINDIR)/mkcapsule: $(MKCAPSULE_SRCS)
+ @mkdir -p $(BINDIR)
+ cc -std=c99 -Wall -Wextra -O2 -I$(ROOT)/v3/include -I$(ROOT)/kernel/include -I$(ROOT)/tools -o $@ $(MKCAPSULE_SRCS)
+
+$(CAPSULE_DIR_C): $(BINDIR)/mkcapsule $(CAPSULE_FILES)
+ $(BINDIR)/mkcapsule $(SIGN_KEY_ARGS) $(ROOT)/capsules $@
+
+define HOSTED_RULE
+$(BINDIR)/starforth4-$(1): $(HERE)/tools/hosted.c $(BINDIR)/v4_image_64.c $(CAPSULE_DIR_C) $$(ENGINE_SRCS) $$(SYSTEM_SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/Makefile
+ $$(CC_$(1)) $$(CFLAGS) -D_POSIX_C_SOURCE=200809L -I$(HERE)/include -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(HERE)/tools/hosted.c -o $(BINDIR)/hosted-$(1).o
+ $$(CC_$(1)) $$(SYSTEM_CFLAGS) -static $(BINDIR)/hosted-$(1).o $(BINDIR)/v4_image_64.c $(CAPSULE_DIR_C) $$(ENGINE_SRCS) $$(SYSTEM_SRCS) -o $$@
+
+$(BINDIR)/boot-$(1).txt: $(BINDIR)/starforth4-$(1)
+ @echo " [hosted $(1)]"
+ @$$(RUN_$(1)) $(BINDIR)/starforth4-$(1) < /dev/null > $$@ || { cat $$@; rm -f $$@; exit 1; }
+ @cat $$@
+endef
+$(foreach i,$(HOSTED_ISAS),$(eval $(call HOSTED_RULE,$(i))))
+
+# `make hosted` builds the three. `make hosted-check` boots each with no
+# input and requires that all three print the same lines, ending in
+# PARITY:OK and the prompt: the same hashes on every ISA.
+.PHONY: hosted hosted-check
+hosted: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/starforth4-$(i))
+hosted-check: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/boot-$(i).txt)
+ @grep -q '^PARITY:OK$$' $(BINDIR)/boot-amd64.txt || { echo "hosted-check: no PARITY:OK"; exit 1; }
+ @cmp $(BINDIR)/boot-amd64.txt $(BINDIR)/boot-aarch64.txt
+ @cmp $(BINDIR)/boot-amd64.txt $(BINDIR)/boot-riscv64.txt
+ @echo "hosted-check: amd64, aarch64 and riscv64 boot identically"
+
.PHONY: all test sanitize clean $(addprefix test-,$(WIDTHS))
all: test
diff --git a/v4/README.md b/v4/README.md
index ebcf0c98..224643e0 100644
--- a/v4/README.md
+++ b/v4/README.md
@@ -20,5 +20,42 @@ input a test feeds) standing in for the console node until the mesh exists. The
onto a node either opcode by opcode (`v4/include/v4/asm.h`) or as text in the notation `DECOMPOSITION.md`
uses (`v4/include/v4/text.h`). `make -C v4 test` builds
and runs the tests at both cell widths; `make -C v4 sanitize` repeats them
-under ASan and UBSan. There is no compiler capsule, no POST and no K
-measurement yet.
+under ASan and UBSan. There is no POST and no K measurement yet.
+
+## The system: nucleus, capsules, prompt
+
+`docs/v4.0.0/NUCLEUS.md` is the design. A v4 system is four things:
+
+| Part | Where | What it is |
+|---|---|---|
+| Engine | `v4/src` | The golden model of the 32-opcode node |
+| Nucleus | `v4/capsule/*.v4`, built by `v4/tools/mkimage.c` | The assembled words, as a memory image linked into the binary |
+| Capsules | `capsules/v4/*.4th`, baked by `tools/mkcapsule.c` | FORTH source, loaded when the system comes up |
+| Boot | `v4/system/boot.c` | Starts the nucleus, checks and loads each capsule, prints the parity lines, gives the prompt |
+
+Two products link the same four and differ only in the console:
+
+- **Hosted Linux**, `v4/tools/hosted.c`: `make -C v4 hosted` builds
+ `v4/build/starforth4-amd64`, `-aarch64` and `-riscv64`, static, 64-bit cells.
+- **Bare metal**, `kernel/src/v4/sk_v4.c`: `make -f kernel/Makefile ARCH= STARFORTH_V4=1`.
+
+A boot prints:
+
+```
+PARITY:V4_NUCLEUS words=292 image_hash=0x...
+PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x... capsule_hash=0x... dict_hash=0x...
+PARITY:OK
+ok>
+```
+
+Every build of one commit prints the same hashes. `make -C v4 hosted-check`
+boots the three hosted binaries (the two foreign ones under user-mode QEMU)
+and fails unless their output is identical and ends in `PARITY:OK`.
+
+A capsule line the node does not answer ` ok` to ends the boot, naming the
+capsule, block and line, with `PARITY:FAIL` and `POST: FAILED`.
+
+State, 2026-10-05: capsule loading works hosted on all three ISAs, and the
+kernel compiles with `STARFORTH_V4=1` on all three. `forth79.4th` holds no
+definitions yet: all 292 words are still in the nucleus. There is no POST
+yet, and no bare-metal boot of v4 has been run.
diff --git a/v4/include/v4/boot.h b/v4/include/v4/boot.h
new file mode 100644
index 00000000..b925c5b0
--- /dev/null
+++ b/v4/include/v4/boot.h
@@ -0,0 +1,50 @@
+/* boot.h -- how a StarForth v4 system comes up: the nucleus, then its
+ * capsules, then the prompt. docs/v4.0.0/NUCLEUS.md.
+ *
+ * The hosted binary (tools/hosted.c) and the bare-metal kernel
+ * (kernel/src/v4/sk_v4.c) both call v4_boot_run and differ only in where
+ * the text goes. So the two start the same way and print the same lines:
+ *
+ * PARITY:V4_NUCLEUS words=N image_hash=0x...
+ * PARITY:V4_CAPSULE name=... capsule_id=0x... capsule_hash=0x... dict_hash=0x...
+ * PARITY:OK
+ * ok>
+ *
+ * or, if anything is wrong, what was wrong and then
+ *
+ * PARITY:FAIL
+ * POST: FAILED
+ *
+ * A capsule is found by name in the directory tools/mkcapsule.c baked into
+ * the binary, its hash is recomputed, its signature is checked -- one that
+ * does not verify refuses the capsule, a missing one is only reported, as
+ * for v3's capsules -- and its blocks are given to the node a line at a
+ * time as if typed. The node must answer " ok" to every line; the first
+ * line it does not accept ends the boot. What a line prints is shown.
+ *
+ * The dictionary hash is FNV-1a over the node's memory below HERE, a cell at
+ * a time, low byte first, then LATEST. It does not depend on the machine:
+ * every build of one commit, with one cell width, prints the same hashes.
+ */
+#ifndef V4_BOOT_H
+#define V4_BOOT_H
+
+#include "v4/image.h"
+
+typedef struct {
+ v4_node *n;
+ v4_exec_state *es;
+ v4_heat *h;
+ const v4_image *im; /* the nucleus */
+ void (*out)(const char *text, unsigned len); /* the console */
+} v4_boot;
+
+/* Start the node from the nucleus image, with `disk` as its block storage
+ * (see v4_image_boot), and load the capsules. Returns 1 with the node
+ * waiting at its prompt, the prompt printed; or 0, the failure printed. */
+int v4_boot_run(const v4_boot *b, unsigned char *disk, unsigned blocks);
+
+/* The dictionary hash of a node started from `im`. */
+uint64_t v4_boot_dict_hash(const v4_node *n, const v4_image *im);
+
+#endif /* V4_BOOT_H */
diff --git a/v4/include/v4/image.h b/v4/include/v4/image.h
new file mode 100644
index 00000000..4e548d8e
--- /dev/null
+++ b/v4/include/v4/image.h
@@ -0,0 +1,71 @@
+/* image.h -- a capsule image: the host node's memory with the vocabulary in
+ * it, and what a loader needs to know to start it.
+ *
+ * The compiler capsule is text (capsule/ *.v4) and FORTH source (capsule/
+ * *.fth). tools/mkimage.c assembles the one and has a node compile the
+ * other, on the build machine, and writes what the node's memory then holds
+ * as a C file. A system that is to run the vocabulary -- the hosted binary,
+ * the bare-metal kernel -- links that file and calls v4_image_boot: it needs
+ * no assembler, no files and no C library.
+ *
+ * An image is for one cell width, one node size and one pair of stack sizes;
+ * v4_image_boot refuses an image the engine was not built for.
+ */
+#ifndef V4_IMAGE_H
+#define V4_IMAGE_H
+
+#include "v4/exec.h"
+
+/* one word of memory that is not zero */
+typedef struct {
+ v4_cell addr;
+ v4_cell value;
+} v4_image_cell;
+
+typedef struct {
+ const v4_image_cell *cells; /* every non-zero word of memory, in address order */
+ unsigned count;
+
+ /* what the image was built for */
+ unsigned cell_bits, node_words, data_ring, ret_ring;
+
+ /* where to start, and where a fault goes */
+ v4_cell entry; /* QUIT */
+ v4_cell fault_table; /* (FAULTS) */
+
+ /* KEY's code: a node whose P is in key_start .. key_end - 1 with no
+ * character pending is waiting for one */
+ v4_cell key_start, key_end;
+
+ /* the memory-mapped registers (DECOMPOSITION.md section 7; D-4 leaves
+ * the map to the loader, and this is the map the image was built with) */
+ v4_cell console_tx, console_rx, console_status;
+ v4_cell dstack_reg, rstack_reg;
+ v4_cell node_error;
+ v4_cell storage_reg;
+
+ /* the dictionary's two variables: DP holds HERE, a byte address, and
+ * LATEST the newest word of FORTH. What lies below HERE, and LATEST,
+ * is what the dictionary hash covers (v4/include/v4/boot.h). */
+ v4_cell dp, latest;
+} v4_image;
+
+/* The image built from v4/capsule (the generated file defines it). */
+extern const v4_image v4_capsule_image;
+
+/* Reset `n`, load the image into it, attach its registers -- with `disk`,
+ * `blocks` blocks of 1024 bytes, as its block storage, or none if `disk` is
+ * 0 -- and leave it about to execute its first instruction. Returns 1, or 0
+ * if the image is not for this build of the engine (nothing is then done).
+ *
+ * After it, the caller runs the node: v4_exec_step_word again and again,
+ * taking what the node prints from n->console (and setting n->console_len
+ * back to 0) and giving it what is typed with v4_node_console_feed. */
+int v4_image_boot(v4_node *n, v4_exec_state *es, v4_heat *h, const v4_image *im,
+ unsigned char *disk, unsigned blocks);
+
+/* 1 if the node is waiting for a character: it is inside KEY and none is
+ * pending. */
+int v4_image_waiting(const v4_node *n, const v4_image *im);
+
+#endif /* V4_IMAGE_H */
diff --git a/v4/src/heat.c b/v4/src/heat.c
index 8d80b1ba..482ddc20 100644
--- a/v4/src/heat.c
+++ b/v4/src/heat.c
@@ -1,12 +1,14 @@
/* heat.c -- heat and anti-clock. See heat.h. */
#include "v4/heat.h"
-#include
void v4_heat_reset(v4_heat *h)
{
- memset(h->op, 0, sizeof(h->op));
- memset(h->call, 0, sizeof(h->call));
- memset(h->call_freeze_mask, 0, sizeof(h->call_freeze_mask));
+ /* Loops, not memset: the engine uses nothing from the C library, so that
+ * the bare-metal kernel can link it as it is. */
+ unsigned i;
+ for (i = 0; i < sizeof h->op / sizeof h->op[0]; i++) h->op[i] = 0;
+ for (i = 0; i < sizeof h->call / sizeof h->call[0]; i++) h->call[i] = 0;
+ for (i = 0; i < sizeof h->call_freeze_mask / sizeof h->call_freeze_mask[0]; i++) h->call_freeze_mask[i] = 0;
}
void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock)
diff --git a/v4/src/image.c b/v4/src/image.c
new file mode 100644
index 00000000..36f2578f
--- /dev/null
+++ b/v4/src/image.c
@@ -0,0 +1,36 @@
+/* image.c -- start a node from a capsule image. See image.h.
+ *
+ * Nothing here uses the C library: the bare-metal kernel links this file.
+ */
+#include "v4/image.h"
+
+int v4_image_boot(v4_node *n, v4_exec_state *es, v4_heat *h, const v4_image *im,
+ unsigned char *disk, unsigned blocks)
+{
+ unsigned i;
+
+ if (im->cell_bits != (unsigned)V4_CELL_BITS || im->node_words != (unsigned)V4_NODE_WORDS
+ || im->data_ring != (unsigned)V4_DATA_RING || im->ret_ring != (unsigned)V4_RET_RING)
+ return 0;
+
+ v4_node_reset(n);
+ v4_exec_reset(es);
+ v4_heat_reset(h);
+ for (i = 0; i < im->count; i++)
+ if (v4_node_addr_ok(im->cells[i].addr)) n->mem[im->cells[i].addr] = im->cells[i].value;
+
+ v4_node_console_attach(n, im->console_tx);
+ v4_node_console_input_attach(n, im->console_rx, im->console_status);
+ v4_node_stack_regs_attach(n, im->dstack_reg, im->rstack_reg);
+ v4_node_error_attach(n, im->node_error);
+ v4_node_fault_attach(n, im->fault_table);
+ if (disk && blocks) v4_node_storage_attach(n, im->storage_reg, disk, blocks);
+
+ n->p = im->entry;
+ return 1;
+}
+
+int v4_image_waiting(const v4_node *n, const v4_image *im)
+{
+ return n->input_pos == n->input_len && n->p >= im->key_start && n->p < im->key_end;
+}
diff --git a/v4/system/boot.c b/v4/system/boot.c
new file mode 100644
index 00000000..e3e0ddf1
--- /dev/null
+++ b/v4/system/boot.c
@@ -0,0 +1,253 @@
+/* boot.c -- the nucleus, then its capsules, then the prompt. See boot.h.
+ *
+ * Nothing here uses the C library: the bare-metal kernel links this file.
+ * It is not part of the engine (v4/src): it needs the capsule directory,
+ * which only a whole system has.
+ */
+#include "v4/boot.h"
+#include "starkernel/capsule.h"
+#include "starkernel/capsule_generated.h"
+#include "starkernel/capsule_sig.h"
+#include "starkernel/capsule_blocks.h"
+
+/* The capsules, in the order they are loaded. */
+static const char *const boot_capsules[] = { "v4:forth79.4th" };
+
+#define LINE_MAX 80u /* QUERY takes 80 characters, the new-line among them */
+#define STEP_LIMIT 4000000000ULL /* instruction words one line may take */
+
+/* ---- printing ------------------------------------------------------------ */
+
+static void say(const v4_boot *b, const char *s)
+{
+ unsigned len = 0;
+ while (s[len]) len++;
+ b->out(s, len);
+}
+
+static void say_dec(const v4_boot *b, uint32_t v)
+{
+ char buf[10];
+ unsigned i = sizeof buf;
+ do { buf[--i] = (char)('0' + v % 10u); v /= 10u; } while (v);
+ b->out(buf + i, (unsigned)sizeof buf - i);
+}
+
+static void say_hex(const v4_boot *b, uint64_t v)
+{
+ char buf[18];
+ unsigned i;
+ buf[0] = '0'; buf[1] = 'x';
+ for (i = 0; i < 16; i++) buf[2 + i] = "0123456789abcdef"[(v >> (60 - 4 * i)) & 15u];
+ b->out(buf, sizeof buf);
+}
+
+/* ---- 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]);
+}
+
+static uint64_t image_hash(const v4_image *im)
+{
+ uint64_t h = FNV_OFFSET;
+ unsigned i;
+ for (i = 0; i < im->count; i++) {
+ h = hash_cell(h, im->cells[i].addr);
+ h = hash_cell(h, im->cells[i].value);
+ }
+ return h;
+}
+
+/* how many words FORTH holds: the list from LATEST, each entry's link in the
+ * cell before its code */
+static uint32_t word_count(const v4_node *n, const v4_image *im)
+{
+ v4_cell xt = n->mem[im->latest];
+ uint32_t count = 0;
+ while (xt > 0 && xt < (v4_cell)V4_NODE_WORDS && count < (uint32_t)V4_NODE_WORDS) {
+ count++;
+ xt = n->mem[xt - 1];
+ }
+ return count;
+}
+
+/* ---- running the node ---------------------------------------------------- */
+
+/* The node ends every line it has taken with " ok" and the next prompt.
+ * Those eight characters are held back from the console, so that what is
+ * shown is only what the line itself printed. */
+static const char accepted[8] = { ' ', 'o', 'k', '\n', 'o', 'k', '>', ' ' };
+
+typedef struct {
+ char held[8];
+ unsigned len;
+} tail;
+
+static void tail_put(const v4_boot *b, tail *t, int show, char c)
+{
+ unsigned i;
+ if (t->len == sizeof t->held) {
+ if (show) b->out(t->held, 1);
+ for (i = 1; i < sizeof t->held; i++) t->held[i - 1] = t->held[i];
+ t->len--;
+ }
+ t->held[t->len++] = c;
+}
+
+/* Run until the node waits for a character. Returns 1 if what it printed
+ * ended with " ok" and the prompt; 0 if not -- the rest is then shown too --
+ * or if the node stopped or never came back. */
+static int run_to_prompt(const v4_boot *b, int show)
+{
+ v4_node *n = b->n;
+ uint64_t steps = 0;
+ unsigned i;
+ tail t;
+
+ t.len = 0;
+ for (;;) {
+ (void)v4_exec_step_word(n, b->es, b->h);
+ if (n->console_len) {
+ for (i = 0; i < n->console_len; i++) tail_put(b, &t, show, (char)n->console[i]);
+ n->console_len = 0;
+ }
+ if (n->stopped) { say(b, "\nV4: the node stopped on a fault\n"); return 0; }
+ if (v4_image_waiting(n, b->im)) break;
+ if (++steps > STEP_LIMIT) { say(b, "\nV4: the node did not come back to its prompt\n"); return 0; }
+ }
+ if (t.len == sizeof t.held) {
+ for (i = 0; i < sizeof t.held && t.held[i] == accepted[i]; i++) { }
+ if (i == sizeof t.held) return 1;
+ }
+ if (show) b->out(t.held, t.len);
+ return 0;
+}
+
+/* ---- one capsule --------------------------------------------------------- */
+
+static void say_where(const v4_boot *b, const char *name, uint32_t block, uint32_t line)
+{
+ say(b, "\nV4: capsule "); say(b, name);
+ say(b, " block "); say_dec(b, block);
+ say(b, " line "); say_dec(b, line);
+}
+
+static int load_capsule(const v4_boot *b, const char *name)
+{
+ const CapsuleDirHeader *dir = capsule_get_directory();
+ const CapsuleDesc *descs = capsule_get_descriptors();
+ const CapsuleNameEntry *names = capsule_get_names();
+ const uint8_t *arena = capsule_get_arena();
+ const CapsuleDesc *cap;
+ const uint8_t *p, *end;
+ CapsuleValidateResult vr;
+ CapsuleSigResult sr;
+ uint32_t block = 0, line = 0;
+ int in_block = 0;
+
+ cap = capsule_find_by_name(dir, descs, names, name);
+ if (!cap) { say(b, "V4: capsule "); say(b, name); say(b, " is not in this binary\n"); return 0; }
+
+ vr = capsule_validate(cap, arena, dir->arena_size, 1);
+ if (vr != CAPSULE_VALID) {
+ say(b, "V4: capsule "); say(b, name); say(b, ": "); say(b, capsule_validate_result_str(vr)); say(b, "\n");
+ return 0;
+ }
+
+ sr = capsule_verify_signature(descs, names, capsule_get_signatures(), arena, dir->desc_count, (int)(cap - descs));
+ if (sr != CAPSULE_SIG_OK) {
+ say(b, "V4: capsule "); say(b, name); say(b, " signature: "); say(b, capsule_sig_result_str(sr)); say(b, "\n");
+ if (sr == CAPSULE_SIG_INVALID) return 0;
+ }
+
+ p = capsule_get_payload(cap, arena);
+ if (!p) { say(b, "V4: capsule "); say(b, name); say(b, " has no payload\n"); return 0; }
+ end = p + cap->length;
+
+ while (p < end) {
+ const uint8_t *after, *nl;
+ uint32_t num;
+ unsigned len, i;
+ char text[LINE_MAX];
+
+ if (capsule_block_header(p, end, &num, &after)) {
+ block = num; line = 0; in_block = 1; p = after;
+ continue;
+ }
+ for (nl = p; nl < end && *nl != '\n'; nl++) { }
+ len = (unsigned)(nl - p);
+ if (len && p[len - 1] == '\r') len--;
+ if (in_block) {
+ line++;
+ if (len >= LINE_MAX) { say_where(b, name, block, line); say(b, " is too long\n"); return 0; }
+ if (len) {
+ for (i = 0; i < len; i++) text[i] = (char)p[i];
+ text[len] = '\n';
+ if (v4_node_console_feed(b->n, text, len + 1u) != len + 1u) {
+ say_where(b, name, block, line); say(b, ": the node's input is full\n");
+ return 0;
+ }
+ if (!run_to_prompt(b, 1)) {
+ say_where(b, name, block, line); say(b, " was not accepted: ");
+ b->out(text, len); say(b, "\n");
+ return 0;
+ }
+ }
+ }
+ p = (nl < end) ? nl + 1 : end;
+ }
+
+ say(b, "PARITY:V4_CAPSULE name="); say(b, name);
+ say(b, " capsule_id="); say_hex(b, cap->capsule_id);
+ say(b, " capsule_hash="); say_hex(b, cap->content_hash);
+ say(b, " dict_hash="); say_hex(b, v4_boot_dict_hash(b->n, b->im));
+ say(b, "\n");
+ return 1;
+}
+
+/* ---- the whole boot ------------------------------------------------------ */
+
+int v4_boot_run(const v4_boot *b, unsigned char *disk, unsigned blocks)
+{
+ unsigned i;
+
+ if (!v4_image_boot(b->n, b->es, b->h, b->im, disk, blocks)) {
+ say(b, "V4: the nucleus image is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
+ return 0;
+ }
+ say(b, "PARITY:V4_NUCLEUS words="); say_dec(b, word_count(b->n, b->im));
+ say(b, " image_hash="); say_hex(b, image_hash(b->im));
+ say(b, "\n");
+
+ /* the node's own first prompt is not shown: the boot prints one at the end */
+ (void)run_to_prompt(b, 0);
+ if (b->n->stopped) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
+
+ for (i = 0; i < sizeof boot_capsules / sizeof boot_capsules[0]; i++)
+ if (!load_capsule(b, boot_capsules[i])) { say(b, "PARITY:FAIL\nPOST: FAILED\n"); return 0; }
+
+ say(b, "PARITY:OK\nok> ");
+ return 1;
+}
diff --git a/v4/tools/hosted.c b/v4/tools/hosted.c
new file mode 100644
index 00000000..f41ca4f8
--- /dev/null
+++ b/v4/tools/hosted.c
@@ -0,0 +1,48 @@
+/* hosted.c -- StarForth v4 as a Linux program: the nucleus image on the
+ * golden model, its capsules loaded as the system comes up (v4/include/v4/
+ * boot.h, the same boot the bare-metal kernel runs), the console on stdin
+ * and stdout.
+ *
+ * It runs until its input ends. Blocks are kept in memory and are gone
+ * when it stops.
+ */
+#include "v4/boot.h"
+#include
+#include
+
+#define BLOCKS 64u
+
+static v4_node n;
+static v4_exec_state es;
+static v4_heat h;
+static unsigned char disk[BLOCKS * V4_BLOCK_BYTES];
+
+static void console_out(const char *text, unsigned len)
+{
+ (void)fwrite(text, 1, len, stdout);
+}
+
+int main(void)
+{
+ const v4_image *im = &v4_capsule_image;
+ unsigned char buf[256];
+ v4_boot boot;
+
+ boot.n = &n; boot.es = &es; boot.h = &h; boot.im = im; boot.out = console_out;
+ if (!v4_boot_run(&boot, disk, BLOCKS)) { fflush(stdout); return 1; }
+ for (;;) {
+ (void)v4_exec_step_word(&n, &es, &h);
+ if (n.console_len) {
+ (void)fwrite(n.console, 1, n.console_len, stdout);
+ n.console_len = 0;
+ }
+ if (n.stopped) { fflush(stdout); fprintf(stderr, "starforth4: the node stopped on a fault\n"); return 1; }
+ if (v4_image_waiting(&n, im)) {
+ ssize_t got;
+ fflush(stdout);
+ got = read(0, buf, sizeof buf);
+ if (got <= 0) { putchar('\n'); return 0; }
+ (void)v4_node_console_feed(&n, buf, (unsigned)got);
+ }
+ }
+}
diff --git a/v4/tools/mkimage.c b/v4/tools/mkimage.c
new file mode 100644
index 00000000..fe3b4dad
--- /dev/null
+++ b/v4/tools/mkimage.c
@@ -0,0 +1,134 @@
+/* mkimage.c -- build the capsule image (include/v4/image.h).
+ *
+ * mkimage OUTPUT.c
+ *
+ * Runs on the build machine. It assembles capsule/ *.v4 -- the nucleus,
+ * docs/v4.0.0/NUCLEUS.md -- onto a host node with the memory map of
+ * tests/host_map.h and starts the node at its prompt. Then it writes every
+ * word of the node's memory that is not zero, and the addresses a loader
+ * needs, as a C file. No FORTH source is compiled here: what is not in the
+ * nucleus is loaded from capsules when the system boots (v4/system/boot.c).
+ *
+ * The image is the same on every machine it is built on: nothing in it
+ * depends on the build machine but the cell width this tool was compiled
+ * for.
+ */
+#include "v4/image.h"
+#include "v4/text.h"
+#include
+#include
+#include
+
+#include "../tests/host_map.h"
+
+static v4_node n;
+static v4_exec_state es;
+static v4_heat h;
+static v4_text tx;
+static v4_cell w_key, w_key_end;
+static unsigned char disk[4 * V4_BLOCK_BYTES];
+
+static void die(const char *what, const char *detail)
+{
+ fprintf(stderr, "mkimage: %s%s%s\n", what, detail ? ": " : "", detail ? detail : "");
+ exit(1);
+}
+
+/* run until the node has taken all its input and is waiting in KEY */
+static void run_until_waiting(void)
+{
+ long steps = 0;
+ unsigned idle = 0;
+ while (idle < 64) {
+ if (++steps > 200000000L) die("the node did not come back to its prompt", NULL);
+ if (n.stopped) die("the node stopped on a fault", NULL);
+ (void)v4_exec_step_word(&n, &es, &h);
+ if (n.input_pos == n.input_len && n.p >= w_key && n.p < w_key_end) idle++; else idle = 0;
+ }
+}
+
+/* every entry from xt back: no access control fields set */
+static void clear_acl(v4_cell xt)
+{
+ for (; xt != 0; xt = n.mem[xt - 1]) n.mem[xt - 3] &= 31;
+}
+
+int main(int argc, char **argv)
+{
+ static const char *const files[] = { "core.v4", "input.v4", "dict.v4", "codegen.v4", "compile.v4", "quit.v4", "forth.v4",
+ "numout.v4", "words.v4", "system.v4", "qmath.v4", "blocks.v4", "log.v4", "acl.v4" };
+ FILE *out;
+ v4_cell k, entry, faults, voc;
+ unsigned count = 0;
+
+ if (argc != 2) die("usage: mkimage OUTPUT.c", NULL);
+
+ if (!host_load(&tx, &n, files, (unsigned)(sizeof files / sizeof files[0]))) die("the capsule does not assemble", NULL);
+ if (!v4_text_finish(&tx)) die("not everything is defined", v4_text_error(&tx));
+ if (v4_text_here(&tx) >= DICT_W) die("the capsule's code runs into the dictionary space", NULL);
+ entry = v4_text_word(&tx, "QUIT");
+ faults = v4_text_word(&tx, "(FAULTS)");
+ w_key = v4_text_word(&tx, "KEY");
+ w_key_end = v4_text_word(&tx, "CR");
+
+ /* the variables, as at switch-on */
+ n.mem[DP] = DICT_W * 4;
+ n.mem[LATEST] = v4_text_latest(&tx);
+ n.mem[STATE] = 0;
+ n.mem[CFP] = CFS_W;
+ n.mem[BASE] = 10;
+ n.mem[FENCE] = DICT_W;
+ n.mem[LOG_LEVEL] = 2;
+ n.mem[CONTEXT] = LATEST;
+ n.mem[CURRENT] = LATEST;
+ n.mem[SRC] = TIB;
+
+ v4_exec_reset(&es);
+ v4_heat_reset(&h);
+ v4_node_console_attach(&n, CONSOLE_TX);
+ v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
+ v4_node_error_attach(&n, NODE_ERROR);
+ v4_node_fault_attach(&n, faults);
+ v4_node_storage_attach(&n, STORAGE_REG, disk, 4);
+ n.p = entry;
+ run_until_waiting();
+
+ /* The nucleus is the system as COLD finds it: COLD comes back to here,
+ * and FORGET will not go below it. What the capsules add at boot
+ * (v4/system/boot.c) is above it. */
+ n.mem[BOOT_CELLS] = n.mem[DP];
+ n.mem[BOOT_CELLS + 1] = n.mem[LATEST];
+ n.mem[FENCE] = (n.mem[DP] + 3) / 4;
+ /* and nothing of the building is left behind: the terminal is the input,
+ * no block is in a buffer, no word has an access control field set */
+ n.mem[NODE_ERROR] = 0;
+ n.mem[BLK] = 0; n.mem[SCR] = 0; n.mem[SRC] = TIB; n.mem[SRC_HOOK] = 0;
+ for (k = BVARS; k < BVARS + 6; k++) n.mem[k] = 0;
+ for (k = STORAGE_REG; k < STORAGE_REG + 4; k++) n.mem[k] = 0;
+ for (k = BUF0_W; k < BUF0_W + 2 * 256; k++) n.mem[k] = 0;
+ for (k = TIB_W; k < TIB_W + 260; k++) n.mem[k] = 0;
+ for (k = WBUF_W; k < WBUF_W + WBUF_CELLS; k++) n.mem[k] = 0;
+ for (k = PAD_W; k < PAD_W + 21; k++) n.mem[k] = 0;
+ n.mem[TO_IN] = 0; n.mem[SPAN] = 0;
+ clear_acl(n.mem[LATEST]);
+ for (voc = n.mem[VOC_LINK]; voc != 0; voc = n.mem[voc + 1]) clear_acl(n.mem[voc]);
+ if (!v4_node_guards_intact(&n)) die("the node's guards are damaged", NULL);
+
+ out = fopen(argv[1], "w");
+ if (!out) die("cannot write", argv[1]);
+ fprintf(out, "/* Generated by v4/tools/mkimage.c from v4/capsule -- do not edit. */\n#include \"v4/image.h\"\n\n");
+ fprintf(out, "static const v4_image_cell cells[] = {\n");
+ for (k = 0; k < (v4_cell)V4_NODE_WORDS; k++)
+ if (n.mem[k] != 0) {
+ fprintf(out, " { %ld, (v4_cell)0x%llxULL },\n", (long)k, (unsigned long long)(v4_ucell)n.mem[k]);
+ count++;
+ }
+ fprintf(out, "};\n\nconst v4_image v4_capsule_image = {\n cells, %uu,\n %uu, %uu, %uu, %uu,\n", count,
+ (unsigned)V4_CELL_BITS, (unsigned)V4_NODE_WORDS, (unsigned)V4_DATA_RING, (unsigned)V4_RET_RING);
+ fprintf(out, " %ld, %ld,\n %ld, %ld,\n", (long)entry, (long)faults, (long)w_key, (long)w_key_end);
+ fprintf(out, " %ld, %ld, %ld,\n %ld, %ld,\n %ld,\n %ld,\n %ld, %ld\n};\n", (long)CONSOLE_TX, (long)CONSOLE_RX, (long)CONSOLE_ST,
+ (long)DSTACK_REG, (long)RSTACK_REG, (long)NODE_ERROR, (long)STORAGE_REG, (long)DP, (long)LATEST);
+ if (fclose(out) != 0) die("cannot write", argv[1]);
+ fprintf(stderr, "mkimage: %u words of memory, dictionary to word %ld, %d-bit cells -> %s\n", count, (long)((n.mem[DP] + 3) / 4), V4_CELL_BITS, argv[1]);
+ return 0;
+}