Files
LithosAnanake/docs/v4.0.0/plans/2026-10-07-step6b-post.md
T
rajamesandClaude Opus 5.5 b04ae55ec5 docs(v4.0.0): step 6b -- POST is the kernel's: design, acceptance and plan
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>
2026-10-07 10:27:47 -04:00

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 with Co-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.4th has 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 note acceptance-test-rules.md. One QEMU at a time, clean before qemu, all three ISAs, logs kept. Never delete a log.

Review Focus

  1. 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.
  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.
  3. 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.
  4. An error in the line that sets the state (DECIMAL FORTH DEFINITIONS). Expected: the case fails by name. Test in Task 2.
  5. 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 keep lines with the T| prefix taken off, and the expectation as data (the mode, the stack list, the output string) where expectation() now returns harness lines. Write v4/system/post_cases.c: one static const char *const array of lines and one static const long long array 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. Remove HARNESS and the block writer; OUTPUT becomes the C file. POST79.md's header line says the cases are in v4/system/post_cases.c.
  • Step 2: Check the count before running it. grep -c '^T{ ' capsules/v4/post79.4th is 538. Run make -C v4 post79; the report says 538 cases, and grep -c '^ { "' v4/system/post_cases.c is 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/null compiles clean.
  • Step 5: Commit feat(v4.0.0): POST's cases as a table the kernel holds and 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 as test_host_quit.c assembles it, a kernel that serves its requests, and a line callback built on the same exchange of messages that test's say uses, returning the raw text and how the line ended. Cases written in the test, each with the verdict it must get:
    • 1 2 + expecting stack 3, no output: passes; 5 DUP . . CR expecting empty stack and 5 5 \n: passes;
    • 1 2 + expecting 4: fails; expecting two values: fails (Review Focus 3);
    • 65 EMIT expecting output B: fails; expecting A: passes;
    • NOSUCHWORD expecting an error: passes; expecting stack empty: fails;
    • 1 2 + expecting an error: fails;
    • a case of two lines, : PQ 7 ; then PQ, expecting 7: passes, and afterwards PQ is still a word on the node;
    • a depth-only case HERE expecting 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 line callback that returns -1 for one case: that case fails, the rest are still judged, and the run returns 0 (Review Focus 1);
    • with BASE left 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=N line match the count; each failing case is named in a POST FAIL: line; no passing case's output reaches say.
  • Step 2: Run make -C v4 run-64-test_host_post.c: fails to build, there is no v4_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 of V4_POST_OUT, an error on any line remembered; then the verdict:
    if (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);
    }
    
    Read the stack before anything is popped when printing a failing case's stack<...>. v4_dstack_reset after 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 outside and 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 becomes v4_post_run with a line callback built on the test's tell, 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 after COLD. In test_host_quit.c add that (CATCH) and (EMIT-HOOK) are unknown words. Run and see them fail.
  • Step 2: boot.c. Split v4_boot_line so that its loop takes where the output goes; v4_boot_line passes the console and POST's callback passes a buffer. After the capsules: v4_post_run; on a failure PARITY:FAIL, POST: FAILED, return 0. Then v4_image_seal, then PARITY:V4_SYSTEM word_count= (the count of FORTH's words that PARITY:V4_NUCLEUS already uses) dict_hash=, then PARITY:OK, POST: PASSED. V4_POST_AT_BOOT=0 still leaves POST out.
  • Step 3: The nucleus. EMIT loses its first line and the NONE: label; (DONE) loses the store to (EMIT-HOOK) and the (CATCH) branch, so an error always ends 2 jump (FINISH); the two header lines, the two constants in host_map.h and their comments go.
  • Step 4: git rm capsules/v4/post79.4th; remove it from host_open in test_host_unit.c. v4/build/mkcapsule --lint capsules/ is clean.
  • Step 5: Run make -C v4 clean, then test, sanitize, hosted-check. Update hosted-check's greps in v4/Makefile if they name the POST capsule's line. All pass; the three hosted programs print the same PARITY:V4_SYSTEM line.
  • Step 6: Acceptance 4 on the product. With one case's expected stack changed by hand in a scratch copy of post_cases.c built into a scratch hosted binary (not committed), the boot names the case, prints PARITY:FAIL and POST: 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 node and 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, the PARITY:V4_SYSTEM line equal to hosted's, T{ unknown, RS1 printing 42 42 before and after COLD. If POST fails for want of dictionary room, stop and report (Review Focus 5).
  • Step 2: MESH.md step 6b as done; NUCLEUS.md section 8's list; v4/README.md "POST"; POST79.md as regenerated.
  • Step 3: Commit with the three logs, feat(v4.0.0): POST is the kernel's -- bare metal, and written up, and push.