NUCLEUS.md 6.3 and section 7 as approved 2026-10-07: the kernel holds the cases as a table and feeds them to the node; a runner judges from outside; what the cases define stays, as in v3; a PARITY:V4_SYSTEM line after POST. The capsule harness and its two nucleus variables are withdrawn (6.3a keeps what they were). MESH.md step 6b has the acceptance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
Step 6b: POST Is the Kernel's — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: The kernel holds POST's cases and feeds them to Hera itself; nothing of POST's harness is in her dictionary.
Architecture: mkpost.py writes a C table of cases instead of a capsule. A runner, v4/system/post.c, sends each case's lines to the node through a callback its host supplies, keeps what the node prints, reads the node's data stack from outside, and judges. boot.c calls it where it loaded the POST capsule. The two nucleus variables the capsule harness needed go.
Tech Stack: C99, Python 3 (v4/tools/mkpost.py), the v4 nucleus dialect, make -C v4, make -f kernel/Makefile.
Spec: docs/v4.0.0/NUCLEUS.md 6.3 and section 7; acceptance in docs/v4.0.0/MESH.md step 6b.
Global Constraints
- Branch
StarForth-v4.0.0, main checkout. No new branch, no stash, no worktree. Commit and push after every task; end messages withCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. - v4 follows the OS as designed: read how v3 does a thing before building it, and report a departure instead of building it.
- No stubs or stand-ins. Do not fix anything in v3.
- The cases are the same 538, with the same lines, as
capsules/v4/post79.4thhas today: the generator's choice of cases and its cutting of lines are not changed. - What the cases define stays in the dictionary; the system is sealed after POST.
- Boot lines, in order:
PARITY:V4_NUCLEUS,PARITY:V4_CAPSULE name=v4:forth79.4th,PARITY:V4_POST tests=N pass=N fail=N,PARITY:V4_SYSTEM word_count=N dict_hash=0x%016llx,PARITY:OK,POST: PASSED. - Before any QEMU run read
.claude/CLAUDE.md"Running / Acceptance" and the memory noteacceptance-test-rules.md. One QEMU at a time,cleanbeforeqemu, all three ISAs, logs kept. Never delete a log.
Review Focus
- A case whose line never ends (a loop, or a word waiting for the keyboard). Expected: the generator already leaves such cases out; the runner treats a line that does not come back as a failed case and POST as failed, and does not hang the boot silently. Test in Task 2.
- A case that prints more than the runner keeps. Expected: it fails by name; nothing is written past the buffer. Test in Task 2.
- A case that leaves more on the stack than the table holds, or a full stack. Expected: compared by depth first; a mismatch fails the case. Test in Task 2.
- An error in the line that sets the state (
DECIMAL FORTH DEFINITIONS). Expected: the case fails by name. Test in Task 2. - The dictionary filling during POST, now that nothing is forgotten. Expected: 538 of 538 with room to spare (measured about 1,290 of 5,632 cells); if a case fails for want of room, stop and report. Checked in Task 4.
Task 1: The generator writes a table
Files: Modify v4/tools/mkpost.py, v4/Makefile (post79 target's comment); Create v4/include/v4/post.h, v4/system/post_cases.c (generated); Regenerate docs/v4.0.0/POST79.md
Interfaces — Produces (post.h):
#define V4_POST_STACK 0 /* the stack and the output are compared */
#define V4_POST_DEPTH 1 /* only how many values are left; the output is not compared */
#define V4_POST_DEPTH_OUTPUT 2 /* how many values, and the output */
#define V4_POST_ERROR 3 /* it must end in an error; nothing else is compared */
typedef struct {
const char *name;
const char *const *lines; /* line_count of them */
unsigned line_count;
int expect; /* V4_POST_* */
const long long *stack; /* depth values, the deepest first */
unsigned depth;
const char *output; /* output_len characters */
unsigned output_len;
} v4_post_case;
extern const v4_post_case v4_post_cases[];
extern const unsigned v4_post_case_count;
- Step 1: In
mkpost.py, keep everything up to the choice and cutting of cases as it is. Replace the writing of blocks: for each case keeplineswith theT|prefix taken off, and the expectation as data (the mode, the stack list, the output string) whereexpectation()now returns harness lines. Writev4/system/post_cases.c: onestatic const char *constarray of lines and onestatic const long longarray of stack values per case, then the table. Escape every character of a line or an output that is not printable ASCII, and"and\, as octal. RemoveHARNESSand the block writer;OUTPUTbecomes the C file.POST79.md's header line says the cases are inv4/system/post_cases.c. - Step 2: Check the count before running it.
grep -c '^T{ ' capsules/v4/post79.4this 538. Runmake -C v4 post79; the report says 538 cases, andgrep -c '^ { "' v4/system/post_cases.cis 538. If the number differs, stop: the generator's choice of cases changed. - Step 3: Check the lines are the same. A one-off script in the scratchpad: the
T|lines of the old capsule, in order with the prefix removed, equal the table's lines in order. Zero differences. - Step 4:
cc -std=c99 -Wall -Wextra -Wpedantic -Werror -Iv4/include -DV4_CELL_BITS=64 -c v4/system/post_cases.c -o /dev/nullcompiles clean. - Step 5: Commit
feat(v4.0.0): POST's cases as a table the kernel holdsand push. (The capsule is still there and still what the boot runs.)
Task 2: The runner
Files: Create v4/system/post.c, v4/tests/test_host_post.c; Modify v4/include/v4/post.h, v4/Makefile (tests named test_host_post*.c link post.c)
Interfaces — Produces:
#define V4_POST_OUT 1024u /* the most of a case's printing that is kept */
typedef struct {
void *self;
v4_node *n; /* the node POST is run on: its data stack is read and emptied */
/* Send the node a line and run it to its end. What it prints is put at
* `out`, at most `cap` characters, and *len is how many it printed in
* all. Returns V4_TEXT_QUIT, V4_TEXT_COMPLETED or V4_TEXT_ERROR
* (message.h), or a negative number if the line did not come back. */
int (*line)(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len);
void (*say)(void *self, const char *text, unsigned len); /* the console */
} v4_post_host;
typedef struct { unsigned tests, pass, fail; } v4_post_tally;
/* Run the cases. Prints a line for each failing case and the PARITY:V4_POST
* line. Returns 1 if none failed. */
int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally);
- Step 1: Write the failing test
test_host_post.c: a host node with the nucleus assembled astest_host_quit.cassembles it, a kernel that serves its requests, and alinecallback built on the same exchange of messages that test'ssayuses, returning the raw text and how the line ended. Cases written in the test, each with the verdict it must get:1 2 +expecting stack3, no output: passes;5 DUP . . CRexpecting empty stack and5 5 \n: passes;1 2 +expecting4: fails; expecting two values: fails (Review Focus 3);65 EMITexpecting outputB: fails; expectingA: passes;NOSUCHWORDexpecting an error: passes; expecting stack empty: fails;1 2 +expecting an error: fails;- a case of two lines,
: PQ 7 ;thenPQ, expecting7: passes, and afterwardsPQis still a word on the node; - a depth-only case
HEREexpecting depth 1: passes; depth 2: fails; - a case printing 2,000 characters: fails by name, and the byte after the runner's buffer is untouched (Review Focus 2);
- a
linecallback that returns -1 for one case: that case fails, the rest are still judged, and the run returns 0 (Review Focus 1); - with
BASEleft at 16 and a non-empty stack before a case, the case still sees decimal and an empty stack; - a callback that makes the state line end in an error: the case fails by name (Review Focus 4).
The tally and the
PARITY:V4_POST tests=N pass=N fail=Nline match the count; each failing case is named in aPOST FAIL:line; no passing case's output reachessay.
- Step 2: Run
make -C v4 run-64-test_host_post.c: fails to build, there is nov4_post_run. - Step 3: Write
post.c. Nothing from the C library (the kernel links it). For each case:v4_dstack_reset; the state line, which must complete; each line in turn, the output appended to one buffer ofV4_POST_OUT, an error on any line remembered; then the verdict:Read the stack before anything is popped when printing a failing case'sif (c->expect == V4_POST_ERROR) ok = errored && came_back; else if (errored || !came_back) ok = 0; else { ok = n->ds.depth == c->depth; if (ok && c->expect == V4_POST_STACK) for (k = c->depth; ok && k-- > 0; ) ok = v4_dstack_pop(&n->ds) == (v4_cell)c->stack[k]; if (ok && c->expect != V4_POST_DEPTH) ok = total == c->output_len && total <= V4_POST_OUT && same(out, c->output, total); }stack<...>.v4_dstack_resetafter every case. - Step 4: Run at 64 and 32 bits and under the sanitizers: 0 failures.
- Step 5: Commit
feat(v4.0.0): the POST runner -- the kernel feeds a case and judges it from outsideand push.
Task 3: The boot runs it, and the hooks go
Files: Modify v4/system/boot.c (the POST capsule out of boot_capsules; post_watch and its variables out; a line function that keeps output; the call of v4_post_run; the PARITY:V4_SYSTEM line), v4/Makefile (SYSTEM_SRCS), kernel/Makefile:623-624 (post.o, post_cases.o), v4/capsule/core.v4 (EMIT), v4/capsule/quit.v4 ((DONE), the two headers), v4/tests/host_map.h, v4/tests/test_host_unit.c; Delete capsules/v4/post79.4th
- Step 1: Tests first. In
test_host_unit.c, Hera's POST becomesv4_post_runwith alinecallback built on the test'stell, and the check is the tally: 538, 538, 0. Add: after it,T{on Hera is an unknown word;RS1(defined by case>R.basic) is a word on Hera and still is afterCOLD. Intest_host_quit.cadd that(CATCH)and(EMIT-HOOK)are unknown words. Run and see them fail. - Step 2:
boot.c. Splitv4_boot_lineso that its loop takes where the output goes;v4_boot_linepasses the console and POST's callback passes a buffer. After the capsules:v4_post_run; on a failurePARITY:FAIL,POST: FAILED, return 0. Thenv4_image_seal, thenPARITY:V4_SYSTEM word_count=(the count of FORTH's words thatPARITY:V4_NUCLEUSalready uses)dict_hash=, thenPARITY:OK,POST: PASSED.V4_POST_AT_BOOT=0still leaves POST out. - Step 3: The nucleus.
EMITloses its first line and theNONE:label;(DONE)loses the store to(EMIT-HOOK)and the(CATCH)branch, so an error always ends2 jump (FINISH); the twoheaderlines, the two constants inhost_map.hand their comments go. - Step 4:
git rm capsules/v4/post79.4th; remove it fromhost_openintest_host_unit.c.v4/build/mkcapsule --lint capsules/is clean. - Step 5: Run
make -C v4 clean, thentest,sanitize,hosted-check. Updatehosted-check's greps inv4/Makefileif they name the POST capsule's line. All pass; the three hosted programs print the samePARITY:V4_SYSTEMline. - Step 6: Acceptance 4 on the product. With one case's expected stack changed by hand in a scratch copy of
post_cases.cbuilt into a scratch hosted binary (not committed), the boot names the case, printsPARITY:FAILandPOST: FAILED, and gives no prompt. - Step 7: Commit
feat(v4.0.0): POST is the kernel's -- the boot feeds the cases, and nothing of the harness is on the nodeand push.
Task 4: Bare metal, and the write-up
- Step 1: Three v4 boots with the typed session of step 6 and, added to it,
T{,RS1,COLD,RS1,HERE .. Each log: POST 538 of 538, thePARITY:V4_SYSTEMline equal to hosted's,T{unknown,RS1printing42 42before and afterCOLD. If POST fails for want of dictionary room, stop and report (Review Focus 5). - Step 2:
MESH.mdstep 6b as done;NUCLEUS.mdsection 8's list;v4/README.md"POST";POST79.mdas regenerated. - Step 3: Commit with the three logs,
feat(v4.0.0): POST is the kernel's -- bare metal, and written up, and push.