feat(v4.0.0): POST passes and is part of the boot; U* and U/MOD
The boot is now nucleus, forth79.4th, POST, prompt, on both products. - forth79.4th: U* and U/MOD, the capsule's first colon definitions. They are in the FORTH-79 Required Word Set and neither v3 nor v4 had them. - post79.4th: 550 cases, 126 of the 130 required words. 443 are v3's with v3's result. The rest follow three rulings (2026-10-05): address- dependent cases are checked for count, not value; where v3 departs from FORTH-79 the standard's result is expected; words v3 has no case for get cases written by hand. v4/tools/post79_rules.py holds each exception with its reason and docs/v4.0.0/POST79.md lists them all. - every case starts from an empty stack, DECIMAL and FORTH DEFINITIONS - the boot requires POST's tally line with fail=0 Verified: tests=550 pass=550 fail=0 and identical PARITY lines on hosted amd64, aarch64 and riscv64 (make -C v4 hosted-check) and on bare metal, clean qemu with STARFORTH_V4=1, on the same three (logs/20261005-1619xx, -1621xx, -1625xx). A U/MOD broken on purpose fails five cases and stops the boot. make -C v4 test passes. Not shown: all words but those two are still assembled, so POST has so far tested the assembled words. Nothing was typed at a bare-metal prompt. Open: PAD 42 OVER ! faults on v4 (D-1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5d6043e37e
commit
2930349bbf
+139
-23
@@ -36,6 +36,9 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
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")
|
||||
@@ -83,15 +86,15 @@ HARNESS = r"""
|
||||
( 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 VARIABLE TBASE
|
||||
VARIABLE TDEP
|
||||
CREATE TBUF 64 ALLOT CREATE TSTK 16 ALLOT CREATE TNAME 8 ALLOT
|
||||
0 T# ! 0 TPASS ! 0 TFAIL ! 0 TOPEN ! 10 TBASE !
|
||||
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 -- )
|
||||
DUP TSUM @ 31 * + 16777215 AND TSUM !
|
||||
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 ;
|
||||
@@ -107,34 +110,38 @@ CREATE TBUF 64 ALLOT CREATE TSTK 16 ALLOT CREATE TNAME 8 ALLOT
|
||||
TBAD @ IF 1 TFAIL +!
|
||||
." POST FAIL: " TN COUNT TYPE T-SHOW CR
|
||||
ELSE 1 TPASS +! THEN
|
||||
0 TOPEN ! 0 (CATCH) ! TBASE @ BASE !
|
||||
0 TOPEN ! 0 (CATCH) !
|
||||
THEN ;
|
||||
=====
|
||||
( T{ name begin a case: clean stack, errors caught )
|
||||
( 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) ! BASE @ TBASE ! DECIMAL ;
|
||||
: 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.
|
||||
@@ -312,13 +319,13 @@ def run_v3(v3, cases):
|
||||
None if v3 did not get through it, else (error, stack, output)."""
|
||||
script = [": T| ; IMMEDIATE", ": TCLR BEGIN DEPTH WHILE DROP REPEAT ;"]
|
||||
for n, case in enumerate(cases):
|
||||
script.append('TCLR ." [[B%d]]"' % n)
|
||||
script.append('TCLR DECIMAL FORTH DEFINITIONS ." [[B%d]]"' % n)
|
||||
script.extend(case["lines"])
|
||||
script.append('." [[E%d]]" BASE @ DECIMAL .S BASE ! ." [[S%d]]"' % (n, n))
|
||||
script.append("BYE")
|
||||
done = subprocess.run([v3, "--log-none"], input="\n".join(script) + "\n", capture_output=True,
|
||||
text=True, errors="replace", timeout=600)
|
||||
text = ANSI.sub("", done.stdout)
|
||||
done = subprocess.run([v3, "--log-none"], input=("\n".join(script) + "\n").encode("latin-1"),
|
||||
capture_output=True, timeout=600)
|
||||
text = ANSI.sub("", done.stdout.decode("latin-1")) # a byte is a character
|
||||
results = []
|
||||
for n, case in enumerate(cases):
|
||||
begin, mid, end = "[[B%d]] ok\nok> " % n, "ok> [[E%d]]" % n, "[[S%d]]" % n
|
||||
@@ -362,6 +369,7 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--v3", default=os.path.join(ROOT, "lfs", "amd64", "starforth"))
|
||||
ap.add_argument("--report", default=None)
|
||||
ap.add_argument("--dump", default=None, help="write what v3 did with every case")
|
||||
args = ap.parse_args()
|
||||
|
||||
required, left_out, cases, seen = set(REQUIRED), [], [], {}
|
||||
@@ -410,21 +418,60 @@ def main():
|
||||
current.extend(part.strip("\n").split("\n"))
|
||||
flush()
|
||||
|
||||
disagreements = []
|
||||
disagreements, applied = [], {"address": 0, "standard": 0, "extra": 0}
|
||||
|
||||
def expectation(error, stack, output, mode="full"):
|
||||
"""The lines that end a case."""
|
||||
if error:
|
||||
return ["->ERR"]
|
||||
if mode == "depth":
|
||||
return ["-> %d }TD" % len(stack)]
|
||||
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="])
|
||||
|
||||
def emit(label, lines, ending):
|
||||
block = ["T{ " + label] + lines + ending
|
||||
if len(current) + len(block) > 16:
|
||||
flush()
|
||||
current.extend(block)
|
||||
|
||||
dump = []
|
||||
for case, (error, stack, output) in zip(cases, results):
|
||||
label = case["label"]
|
||||
dump.append("%s\n input: %s\n v3: %s stack=%s out=%r\n"
|
||||
% (label, " ".join(case["text"].split()), "ERROR" if error else "ok", stack, output))
|
||||
if error != case["should_error"]:
|
||||
disagreements.append((case["label"], "v3's table says %s, v3 %s"
|
||||
disagreements.append((label, "v3's table says %s, v3 %s"
|
||||
% ("error" if case["should_error"] else "no error",
|
||||
"raised one" if error else "raised none")))
|
||||
lines = ["T{ " + case["label"]] + case["lines"]
|
||||
if error:
|
||||
lines.append("->ERR")
|
||||
if label in rules.STANDARD:
|
||||
rule = rules.STANDARD[label]
|
||||
if rule is None: # the case is dropped
|
||||
applied["standard"] += 1
|
||||
left_out.append((label, rules.DROPPED[label]))
|
||||
continue
|
||||
lines = pack("T|", atoms(rule.get("text", case["text"])))
|
||||
assert lines, label
|
||||
if "depth" in rule:
|
||||
emit(label, lines, ["-> %d }TD" % rule["depth"]])
|
||||
else:
|
||||
emit(label, lines, expectation(rule.get("error", False), rule.get("stack", []), rule.get("out", "")))
|
||||
applied["standard"] += 1
|
||||
elif label in rules.ADDRESS:
|
||||
emit(label, case["lines"], expectation(error, stack, output, rules.ADDRESS[label]))
|
||||
applied["address"] += 1
|
||||
else:
|
||||
tail = ["->"] + [str(v) for v in stack] + ["}T", str(len(output)), str(checksum(output)), "T="]
|
||||
lines.extend(pack("", tail))
|
||||
if len(current) + len(lines) > 16:
|
||||
flush()
|
||||
current.extend(lines)
|
||||
emit(label, case["lines"], expectation(error, stack, output))
|
||||
for rule in rules.EXTRA:
|
||||
lines = pack("T|", atoms(rule["text"]))
|
||||
assert lines, rule
|
||||
emit(rule["label"], lines, expectation(rule.get("error", False), rule.get("stack", []), rule.get("out", "")))
|
||||
applied["extra"] += 1
|
||||
unknown = [k for k in list(rules.STANDARD) + list(rules.ADDRESS) if k not in {c["label"] for c in cases}]
|
||||
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"])
|
||||
|
||||
@@ -436,9 +483,15 @@ def main():
|
||||
assert len(line) <= LINE_MAX, line
|
||||
out.write(line + "\n")
|
||||
|
||||
tested = sorted({c["word"] for c in cases})
|
||||
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)]
|
||||
report = ["POST cases: %d, in %d blocks (%d to %d)" % (len(cases), len(blocks), FIRST_BLOCK, FIRST_BLOCK + len(blocks) - 1),
|
||||
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),
|
||||
" 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),
|
||||
" written by hand (post79_rules.EXTRA): %d" % applied["extra"],
|
||||
"Required words with at least one case: %d of %d" % (len(tested), len(REQUIRED)),
|
||||
"Required words with no case: " + " ".join(untested), "",
|
||||
"Left out (%d):" % len(left_out)]
|
||||
@@ -446,10 +499,73 @@ def main():
|
||||
report += ["", "v3 did not do what its own table expects (%d):" % len(disagreements)]
|
||||
report += [" %-32s %s" % item for item in disagreements]
|
||||
text = "\n".join(report) + "\n"
|
||||
write_doc(cases, results, left_out, disagreements, untested, total, applied, dropped)
|
||||
if args.report:
|
||||
open(args.report, "w").write(text)
|
||||
sys.stdout.write(text)
|
||||
|
||||
|
||||
DOC = os.path.join(ROOT, "docs", "v4.0.0", "POST79.md")
|
||||
|
||||
|
||||
def code(text):
|
||||
return "`" + " ".join(text.split()).replace("|", "\\|") + "`"
|
||||
|
||||
|
||||
def expects(rule):
|
||||
if rule.get("error"):
|
||||
return "an error"
|
||||
if "depth" in rule:
|
||||
return "%d value(s) left; output not compared" % rule["depth"]
|
||||
return "stack `%s`, prints %s" % (" ".join(str(v) for v in rule.get("stack", [])) or "empty",
|
||||
"`%s`" % rule["out"].replace("\n", "\\n") if rule.get("out") else "nothing")
|
||||
|
||||
|
||||
def write_doc(cases, results, left_out, disagreements, untested, total, applied, dropped):
|
||||
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`.", "",
|
||||
"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), "",
|
||||
"v3 departs from the standard, or the case is written for v3's machine. v4",
|
||||
"follows the standard (standing ruling), so the expected result is the",
|
||||
"standard's and not v3's.", "",
|
||||
"| Case | v3's line | What v3 did | As POST runs it | Must do | Why |", "|---|---|---|---|---|---|"]
|
||||
for label, rule in rules.STANDARD.items():
|
||||
if rule is None:
|
||||
continue
|
||||
case, (error, stack, output) = by_label[label]
|
||||
did = "error" if error else "stack `%s`, printed `%s`" % (" ".join(map(str, stack)) or "empty", output.replace("\n", "\\n")[:40])
|
||||
ran = code(rule["text"]) if "text" in rule else "unchanged"
|
||||
out.append("| `%s` | %s | %s | %s | %s | %s |" % (label, code(case["text"]), did, ran, expects(rule), rule["why"]))
|
||||
out += ["", "## 2. Address-dependent (%d)" % applied["address"], "",
|
||||
"The case leaves or prints a memory address, which is a different number on",
|
||||
"the two machines. The number of values left is checked, and what is",
|
||||
"printed unless an address is printed; the values are not.", "",
|
||||
"| Case | Line | Checked |", "|---|---|---|"]
|
||||
for label, mode in rules.ADDRESS.items():
|
||||
out.append("| `%s` | %s | %s |" % (label, code(by_label[label][0]["text"]),
|
||||
"values left, and output" if mode == "depth+output" else "values left only"))
|
||||
out += ["", "## 3. Written by hand (%d)" % len(rules.EXTRA), "",
|
||||
"For required words v3 has no case for. The expected results are the",
|
||||
"standard's; no v3 run stands behind them.", "",
|
||||
"| Case | Line | Must do |", "|---|---|---|"]
|
||||
for rule in rules.EXTRA:
|
||||
out.append("| `%s` | %s | %s |" % (rule["label"], code(rule["text"]), expects(rule)))
|
||||
out += ["", "## 4. v3 cases left out (%d)" % len(left_out), "", "| Case | Why |", "|---|---|"]
|
||||
out += ["| `%s` | %s |" % item for item in left_out]
|
||||
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.", "",
|
||||
"## 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]
|
||||
open(DOC, "w").write("\n".join(out) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user