feat(v4.0.0): POST's cases as a table the kernel holds

mkpost.py writes v4/system/post_cases.c where it wrote the capsule: for
each case its name, its lines, and what it must do -- end in an error, or
leave v3's stack and print v3's output, which is now held in full where
the capsule held its length and a checksum.

The same 538 cases: checked against capsules/v4/post79.4th case by case --
names, lines, stacks, and the length and checksum of each output -- with
no difference.  The capsule is still what the boot runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 10:34:03 -04:00
co-authored by Claude Opus 5.5
parent b04ae55ec5
commit c2f4b9808a
4 changed files with 1190 additions and 126 deletions
+5 -6
View File
@@ -1,7 +1,7 @@
# POST for the FORTH-79 Required Word Set: where it departs from v3
Written by `v4/tools/mkpost.py` from `v4/tools/post79_rules.py`; do not edit.
Design: `NUCLEUS.md` section 6. The capsule is `capsules/v4/post79.4th`.
Design: `NUCLEUS.md` section 6. The cases are `v4/system/post_cases.c`.
POST runs 538 cases. 431 are v3's, with what the hosted v3 binary did as the
expected result. This file lists every other case, and why.
@@ -60,11 +60,11 @@ standard's and not v3's.
| `CMOVE.basic` | `HERE S" Test" DUP >R HERE 10 + SWAP CMOVE CR` | stack `325`, printed `\n` | `65 PAD C! 66 PAD 1+ C! PAD PAD 10 + 2 CMOVE PAD 10 + C@ . PAD 11 + C@ . CR` | stack `empty`, prints `65 66 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and the v3 case leaves a value on the return stack |
| `TYPE.basic_string` | `HERE S" Hello" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `400`, printed `Hello\n` | `72 PAD C! 105 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `Hi\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `TYPE.numbers` | `HERE S" 12345" DUP >R HERE SWAP CMOVE HERE R> TYPE CR` | stack `406`, printed `12345\n` | `49 PAD C! 50 PAD 1+ C! PAD 2 TYPE CR` | stack `empty`, prints `12\n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>IN.initial` | `>IN @ . CR` | stack `empty`, printed `1009040112 \n` | unchanged | stack `empty`, prints `9 \n` | FORTH-79: >IN is the offset into the input, 9 here; v3 prints an address |
| `>IN.initial` | `>IN @ . CR` | stack `empty`, printed `860486384 \n` | unchanged | stack `empty`, prints `9 \n` | FORTH-79: >IN is the offset into the input, 9 here; v3 prints an address |
| `UPDATE.no_block` | `0 SCR ! UPDATE` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for |
| `SCR.after_load` | `1 LOAD SCR @ . CR` | stack `empty`, printed `` | unchanged | stack `empty`, prints `1 \n` | v3 prints nothing after 1 LOAD; SCR is still 1 from the LIST before |
| `FIND.existing` | `FIND DUP . CR` | stack `empty`, printed `1009039472 \n` | unchanged | 0 value(s) left; output not compared | FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed |
| `FIND.user_word` | `: test5 44 ; FIND test5 . CR` | stack `empty`, printed `1009366512 \n` | unchanged | 0 value(s) left; output not compared | the address FIND leaves is printed |
| `FIND.existing` | `FIND DUP . CR` | stack `empty`, printed `860485744 \n` | unchanged | 0 value(s) left; output not compared | FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed |
| `FIND.user_word` | `: test5 44 ; FIND test5 . CR` | stack `empty`, printed `860812560 \n` | unchanged | 0 value(s) left; output not compared | the address FIND leaves is printed |
| `FIND.empty` | `FIND` | error | unchanged | stack `0`, prints nothing | FORTH-79: FIND leaves 0 when there is no word; v3 raises an error |
| `VOCABULARY.cross_vocab_access` | `VOCABULARY V1 V1 DEFINITIONS : V1WORD 11 ; FORTH V1WORD . CR` | stack `empty`, printed `11 \n` | unchanged | an error | FORTH-79: a word defined in another vocabulary is not found from FORTH; v3 finds it |
| `VOCABULARY.duplicate` | `VOCABULARY TESTVOC VOCABULARY TESTVOC` | error | unchanged | stack `empty`, prints nothing | v3 raises an error FORTH-79 does not ask for: defining a name again |
@@ -188,8 +188,7 @@ standard's; no v3 run stands behind them.
`KEY` `EXPECT` `QUERY` `QUIT`.
`KEY`, `EXPECT` and `QUERY` wait for the keyboard, which a boot cannot type
at. `QUIT` returns to the terminal without `ok`, which the capsule loader
takes for a refused line.
at. `QUIT` returns to the terminal without `ok`.
## 6. Where v3 did not do what its own table expects (6)
+32
View File
@@ -0,0 +1,32 @@
/* post.h -- POST, which is the kernel's. docs/v4.0.0/NUCLEUS.md section 6.
*
* As in v3, the kernel holds the cases and runs them on the node: nothing
* of POST is in the node's dictionary. A case is a name, its lines of
* FORTH, and what it must do. The table is generated from v3's own cases
* (v4/tools/mkpost.py -> v4/system/post_cases.c).
*/
#ifndef V4_POST_H
#define V4_POST_H
#include "v4/node.h"
#define V4_POST_STACK 0 /* its data stack and what it prints are compared */
#define V4_POST_DEPTH 1 /* only how many values it leaves; what it prints is not compared */
#define V4_POST_DEPTH_OUTPUT 2 /* how many values it leaves, and what it prints */
#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, sent one at a time */
unsigned line_count;
int expect; /* V4_POST_* */
const long long *stack; /* V4_POST_STACK: depth values, the deepest first; else 0 */
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;
#endif /* V4_POST_H */
File diff suppressed because it is too large Load Diff
+54 -120
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""mkpost.py -- write capsules/v4/post79.4th from v3's POST cases.
"""mkpost.py -- write v4/system/post_cases.c from v3's POST cases.
v4/tools/mkpost.py [--v3 PATH] [--report FILE]
@@ -7,7 +7,6 @@ A development tool, run when the cases change; its output is committed and
reviewed like any source. docs/v4.0.0/NUCLEUS.md section 6.
What it does:
1. Reads v3's case tables, v3/src/test_runner/modules/*.c, in the order
v3's own POST runs them. A case is a word, a name, a line of FORTH and
whether an error is expected.
@@ -15,20 +14,16 @@ What it does:
3. Runs every kept case, in order, in one session of the hosted v3 binary,
and records what v3 did: an error or not, the data stack, and what was
printed. v3 is the reference.
4. Writes the harness and one case after another as a .4th capsule:
T{ DUP.basic name the case, start clean
T| 5 DUP . . a line of the case, its output captured
-> 5 5 }T 0 0 T= v3's stack; v3's output: length, checksum
or, for a case v3 ended with an error, ->ERR in place of the last
line.
4. Writes the cases as a table in C, which the kernel holds and feeds to
the node (v4/system/post.c): for each its name, its lines, and what it
must do -- end in an error, or leave v3's stack and print v3's output.
A case is left out, and listed in the report, when: its word reads the
keyboard; it forgets a word of the system or damages it on purpose; v3 could
not finish it in one
session; or one of its lines cannot be cut to fit a 64-character block
line.
not finish it in one session; or one of its lines cannot be cut to fit a
64-character line. (The cases were a capsule of 64-character block lines
until MESH.md step 6b; the cutting is kept so that they are the same cases
and the same lines.)
"""
import argparse
import os
@@ -41,8 +36,7 @@ import post79_rules as rules # noqa: E402
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
MODULE_DIR = os.path.join(ROOT, "v3", "src", "test_runner", "modules")
OUTPUT = os.path.join(ROOT, "capsules", "v4", "post79.4th")
FIRST_BLOCK = 7000
OUTPUT = os.path.join(ROOT, "v4", "system", "post_cases.c")
LINE_MAX = 64
# v3's POST order (v3/src/test_runner/test_runner.c, test_modules[]), the
@@ -79,77 +73,6 @@ CONTEXT CURRENT FORTH DEFINITIONS ' FIND FORGET
# Words whose cases wait for the keyboard, which a boot cannot type at.
KEYBOARD = {"KEY", "EXPECT", "QUERY"}
# ---- the harness: blocks FIRST_BLOCK up, FORTH-79 and the two nucleus hooks ----
HARNESS = r"""
( post79.4th -- POST for the FORTH-79 Required Word Set on v4. )
( Generated by v4/tools/mkpost.py from v3's POST cases: do not )
( edit. docs/v4.0.0/NUCLEUS.md section 6. )
VARIABLE T# VARIABLE TPASS VARIABLE TFAIL VARIABLE TOPEN
VARIABLE TBAD VARIABLE TWANT VARIABLE TLEN VARIABLE TSUM
VARIABLE TDEP
CREATE TBUF 64 ALLOT CREATE TSTK 16 ALLOT CREATE TNAME 8 ALLOT
0 T# ! 0 TPASS ! 0 TFAIL ! 0 TOPEN !
: TB ( -- baddr ) TBUF 4 * ;
: TN ( -- baddr ) TNAME 4 * ;
( T-EMIT is given every character a case prints: the first 255 )
( are kept, all are counted and summed. )
: T-EMIT ( c -- )
255 AND DUP TSUM @ 31 * + 16777215 AND TSUM !
TLEN @ 255 < IF TB TLEN @ + C! ELSE DROP THEN 1 TLEN +! ;
=====
: T-CLEAR ( i*x -- ) BEGIN DEPTH WHILE DROP REPEAT ;
: T. ( n -- ) 0 <# #S #> TYPE ;
( what a failing case printed, and the stack it left )
: T-SHOW ( -- )
." out<" TB TLEN @ 255 MIN TYPE ." > stack<"
TDEP @ BEGIN DUP WHILE 1- DUP TSTK + @ . REPEAT DROP ." >" ;
( the verdict on the case that is open, if one is )
: T-CLOSE ( -- )
TOPEN @ IF
(CATCH) @ -1 = TWANT @ = 0= IF 1 TBAD ! THEN
TBAD @ IF 1 TFAIL +!
." POST FAIL: " TN COUNT TYPE T-SHOW CR
ELSE 1 TPASS +! THEN
0 TOPEN ! 0 (CATCH) !
THEN ;
=====
( T{ name begin a case: clean stack, decimal, FORTH )
: T{ ( i*x -- ) T-CLOSE T-CLEAR
DECIMAL [COMPILE] FORTH DEFINITIONS
BL WORD COUNT 31 MIN DUP TN C! TN 1+ SWAP CMOVE
1 T# +! 1 TOPEN ! 0 TBAD ! 0 TWANT ! 0 TLEN ! 0 TSUM !
0 TDEP ! 1 (CATCH) ! ;
( T| ... a line of the case, what it prints captured )
: T| ' T-EMIT (EMIT-HOOK) ! ; IMMEDIATE
: T-END ( -- ) 0 (EMIT-HOOK) ! DECIMAL ;
( -> ... the case is over: set its stack aside, top first )
: -> ( i*x -- ) T-END 0
BEGIN DEPTH 1 > WHILE
DUP 16 < IF SWAP OVER TSTK + ! ELSE SWAP DROP 1 TBAD ! THEN
1+ REPEAT
16 MIN TDEP ! ;
=====
( ->ERR the case is over, and must have ended in an error )
: ->ERR ( i*x -- ) T-END -1 TWANT ! T-CLEAR ;
( ... }T the stack the case must have left, in order )
: }T ( i*x -- )
DEPTH TDEP @ = 0= IF 1 TBAD ! T-CLEAR EXIT THEN
0 BEGIN DEPTH 1 > WHILE
SWAP OVER TSTK + @ = 0= IF 1 TBAD ! THEN 1+ REPEAT DROP ;
( n }TD only how many values the case must have left )
: }TD ( n -- ) TDEP @ = 0= IF 1 TBAD ! THEN ;
( len sum T= what the case must have printed )
: T= ( len sum -- ) TSUM @ = 0= IF 1 TBAD ! THEN
TLEN @ = 0= IF 1 TBAD ! THEN ;
=====
( the tally; a failure is an error, which ends the boot )
: T-REPORT ( -- ) T-CLOSE 0 (CATCH) ! DECIMAL
." PARITY:V4_POST tests=" T# @ T. ." pass=" TPASS @ T.
." fail=" TFAIL @ T. CR
TFAIL @ IF -1 NODE-ERROR ! THEN ;
"""
# ---- reading v3's case tables ----------------------------------------------
TOKEN = re.compile(r'''
@@ -407,34 +330,24 @@ def main():
left_out.append((cases[first]["label"], "v3 did not get through it in one session"))
del cases[first]
blocks, current = [], []
def flush():
if current:
blocks.append(list(current))
del current[:]
for part in HARNESS.strip("\n").split("=====\n"):
current.extend(part.strip("\n").split("\n"))
flush()
table = []
disagreements, applied = [], {"address": 0, "standard": 0, "extra": 0}
def expectation(error, stack, output, mode="full"):
"""The lines that end a case."""
"""What a case must do."""
if error:
return ["->ERR"]
return {"expect": "V4_POST_ERROR", "stack": [], "depth": 0, "out": ""}
if mode == "depth":
return ["-> %d }TD" % len(stack)]
return {"expect": "V4_POST_DEPTH", "stack": [], "depth": len(stack), "out": ""}
if mode == "depth+output":
return pack("", ["->", str(len(stack)), "}TD", str(len(output)), str(checksum(output)), "T="])
return pack("", ["->"] + [str(v) for v in stack] + ["}T", str(len(output)), str(checksum(output)), "T="])
return {"expect": "V4_POST_DEPTH_OUTPUT", "stack": [], "depth": len(stack), "out": output}
return {"expect": "V4_POST_STACK", "stack": list(stack), "depth": len(stack), "out": output}
def emit(label, lines, ending):
block = ["T{ " + label] + lines + ending
if len(current) + len(block) > 16:
flush()
current.extend(block)
# the lines were cut for a block, each begun "T| ": the same lines, without it
assert all(line.startswith("T| ") for line in lines), lines
table.append((label, [line[3:] for line in lines], ending))
dump = []
for case, (error, stack, output) in zip(cases, results):
@@ -454,7 +367,7 @@ def main():
lines = pack("T|", atoms(rule.get("text", case["text"])))
assert lines, label
if "depth" in rule:
emit(label, lines, ["-> %d }TD" % rule["depth"]])
emit(label, lines, {"expect": "V4_POST_DEPTH", "stack": [], "depth": rule["depth"], "out": ""})
else:
emit(label, lines, expectation(rule.get("error", False), rule.get("stack", []), rule.get("out", "")))
applied["standard"] += 1
@@ -472,22 +385,14 @@ def main():
assert not unknown, "rules for cases that do not exist: %s" % unknown
if args.dump:
open(args.dump, "w").write("\n".join(dump))
flush()
blocks.append(["T-REPORT", "FORGET T# DECIMAL"])
with open(OUTPUT, "w", encoding="ascii") as out:
for k, block in enumerate(blocks):
assert len(block) <= 16, block
out.write("Block %d\n" % (FIRST_BLOCK + k))
for line in block:
assert len(line) <= LINE_MAX, line
out.write(line + "\n")
write_table(table)
tested = sorted({c["word"] for c in cases} | {r["label"].rsplit(".", 1)[0] for r in rules.EXTRA})
untested = [w for w in REQUIRED if w not in set(tested)]
dropped = sum(1 for v in rules.STANDARD.values() if v is None)
total = len(cases) - dropped + len(rules.EXTRA)
report = ["POST cases: %d, in %d blocks (%d to %d)" % (total, len(blocks), FIRST_BLOCK, FIRST_BLOCK + len(blocks) - 1),
assert total == len(table), (total, len(table))
report = ["POST cases: %d, in %s" % (total, os.path.relpath(OUTPUT, ROOT)),
" from v3 unchanged: %d" % (len(cases) - applied["address"] - applied["standard"]),
" from v3, address-dependent (post79_rules.ADDRESS): %d" % applied["address"],
" rewritten or dropped for FORTH-79 (post79_rules.STANDARD): %d, of which dropped %d" % (applied["standard"], dropped),
@@ -505,6 +410,36 @@ def main():
sys.stdout.write(text)
def c_string(text):
"""`text` as a C string literal: everything that is not plain is octal."""
out = []
for ch in text.encode("latin-1", errors="replace"):
if 32 <= ch < 127 and ch not in (34, 92, 63): # not " \ or ? (no trigraphs)
out.append(chr(ch))
else:
out.append("\\%03o" % ch)
return '"' + "".join(out) + '"'
def write_table(table):
with open(OUTPUT, "w", encoding="ascii") as out:
out.write("/* post_cases.c -- POST's cases for the FORTH-79 Required Word Set on v4.\n"
" * Generated by v4/tools/mkpost.py from v3's POST cases: do not edit.\n"
" * docs/v4.0.0/NUCLEUS.md section 6; docs/v4.0.0/POST79.md lists every case\n"
" * that is not v3's own with v3's result.\n */\n"
'#include "v4/post.h"\n\n')
for k, (label, lines, want) in enumerate(table):
out.write("static const char *const l%d[] = { %s };\n" % (k, ", ".join(c_string(line) for line in lines)))
if want["stack"]:
out.write("static const long long s%d[] = { %s };\n" % (k, ", ".join("%dLL" % v if v != -9223372036854775808 else "(-9223372036854775807LL - 1)" for v in want["stack"])))
out.write("\nconst v4_post_case v4_post_cases[] = {\n")
for k, (label, lines, want) in enumerate(table):
out.write(" { %s, l%d, %du, %s, %s, %du, %s, %du },\n"
% (c_string(label), k, len(lines), want["expect"], ("s%d" % k) if want["stack"] else "0", want["depth"],
c_string(want["out"]), len(want["out"].encode("latin-1", errors="replace"))))
out.write("};\n\nconst unsigned v4_post_case_count = %du;\n" % len(table))
DOC = os.path.join(ROOT, "docs", "v4.0.0", "POST79.md")
@@ -525,7 +460,7 @@ def write_doc(cases, results, left_out, disagreements, untested, total, applied,
by_label = {c["label"]: (c, r) for c, r in zip(cases, results)}
out = ["# POST for the FORTH-79 Required Word Set: where it departs from v3", "",
"Written by `v4/tools/mkpost.py` from `v4/tools/post79_rules.py`; do not edit.",
"Design: `NUCLEUS.md` section 6. The capsule is `capsules/v4/post79.4th`.", "",
"Design: `NUCLEUS.md` section 6. The cases are `v4/system/post_cases.c`.", "",
"POST runs %d cases. %d are v3's, with what the hosted v3 binary did as the" % (total, len(cases) - applied["address"] - applied["standard"]),
"expected result. This file lists every other case, and why.", "",
"## 1. Rewritten for FORTH-79 (%d)" % (applied["standard"] - dropped), "",
@@ -559,8 +494,7 @@ def write_doc(cases, results, left_out, disagreements, untested, total, applied,
out += ["", "## 5. Required words POST does not test", "",
" ".join("`%s`" % w for w in untested) + ".", "",
"`KEY`, `EXPECT` and `QUERY` wait for the keyboard, which a boot cannot type",
"at. `QUIT` returns to the terminal without `ok`, which the capsule loader",
"takes for a refused line.", "",
"at. `QUIT` returns to the terminal without `ok`.", "",
"## 6. Where v3 did not do what its own table expects (%d)" % len(disagreements), "",
"POST expects what v3 did, unless section 1 says otherwise.", "", "| Case | |", "|---|---|"]
out += ["| `%s` | %s |" % item for item in disagreements]