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>
572 lines
24 KiB
Python
Executable File
572 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""mkpost.py -- write capsules/v4/post79.4th from v3's POST cases.
|
|
|
|
v4/tools/mkpost.py [--v3 PATH] [--report FILE]
|
|
|
|
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.
|
|
2. Keeps the cases for words of the FORTH-79 Required Word Set.
|
|
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.
|
|
|
|
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.
|
|
"""
|
|
import argparse
|
|
import os
|
|
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")
|
|
FIRST_BLOCK = 7000
|
|
LINE_MAX = 64
|
|
|
|
# v3's POST order (v3/src/test_runner/test_runner.c, test_modules[]), the
|
|
# modules that test FORTH-79 words.
|
|
MODULES = [
|
|
"stack_words_test.c", "return_stack_words_test.c", "memory_words_test.c",
|
|
"arithmetic_words_test.c", "logical_words_test.c",
|
|
"mixed_arithmetic_words_test.c", "double_words_test.c",
|
|
"format_words_test.c", "string_words_test.c", "io_words_test.c",
|
|
"block_words_test.c", "dictionary_words_test.c",
|
|
"dictionary_manipulation_words_test.c", "vocabulary_words_test.c",
|
|
"system_words_test.c", "defining_words_tests.c", "control_words_test.c",
|
|
]
|
|
|
|
# The FORTH-79 Standard's Required Word Set, by the standard's own groups.
|
|
# Written from the standard's word list; check it against the document
|
|
# before relying on it for a claim of compliance.
|
|
REQUIRED = """
|
|
DUP DROP SWAP OVER ROT PICK ROLL ?DUP DEPTH >R R> R@
|
|
< = > 0< 0= 0> D< U< NOT
|
|
+ - * / MOD /MOD */ */MOD 1+ 1- 2+ 2- MAX MIN ABS NEGATE D+ DNEGATE
|
|
U* U/MOD AND OR XOR
|
|
@ ! C@ C! ? +! MOVE CMOVE FILL
|
|
DO LOOP +LOOP I J LEAVE IF ELSE THEN BEGIN UNTIL WHILE REPEAT EXIT EXECUTE
|
|
CR EMIT SPACE SPACES TYPE COUNT -TRAILING KEY EXPECT QUERY WORD
|
|
BASE DECIMAL . U. CONVERT <# # #S HOLD SIGN #>
|
|
LIST LOAD SCR BLOCK UPDATE BUFFER SAVE-BUFFERS EMPTY-BUFFERS
|
|
: ; VARIABLE CONSTANT VOCABULARY CREATE DOES>
|
|
CONTEXT CURRENT FORTH DEFINITIONS ' FIND FORGET
|
|
, ALLOT ." IMMEDIATE LITERAL STATE [ ] COMPILE [COMPILE]
|
|
( >IN ABORT QUIT HERE PAD BLK 79-STANDARD
|
|
""".split()
|
|
|
|
# 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'''
|
|
\s+ | /\*.*?\*/ | //[^\n]* |
|
|
(?P<str>"(?:\\.|[^"\\])*") |
|
|
(?P<punct>[{}(),;=\[\]&*]) |
|
|
(?P<word>[A-Za-z0-9_.+\-]+)
|
|
''', re.S | re.X)
|
|
|
|
ESCAPES = {"n": "\n", "t": "\t", "\\": "\\", '"': '"', "r": "\r", "0": "\0"}
|
|
|
|
|
|
def unescape(lit):
|
|
out, i, body = [], 0, lit[1:-1]
|
|
while i < len(body):
|
|
if body[i] == "\\" and i + 1 < len(body):
|
|
out.append(ESCAPES.get(body[i + 1], body[i + 1]))
|
|
i += 2
|
|
else:
|
|
out.append(body[i])
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
def tokens(text):
|
|
pos, out = 0, []
|
|
while pos < len(text):
|
|
m = TOKEN.match(text, pos)
|
|
if not m:
|
|
pos += 1
|
|
continue
|
|
pos = m.end()
|
|
if m.group("str"):
|
|
value = unescape(m.group("str"))
|
|
if out and out[-1][0] == "str": # adjacent literals join
|
|
out[-1] = ("str", out[-1][1] + value)
|
|
else:
|
|
out.append(("str", value))
|
|
elif m.group("punct"):
|
|
out.append(("punct", m.group("punct")))
|
|
elif m.group("word"):
|
|
out.append(("word", m.group("word")))
|
|
return out
|
|
|
|
|
|
def parse_braces(toks, i):
|
|
"""toks[i] is '{'; returns (nested list, index after the matching '}')."""
|
|
items = []
|
|
i += 1
|
|
while toks[i] != ("punct", "}"):
|
|
if toks[i] == ("punct", "{"):
|
|
sub, i = parse_braces(toks, i)
|
|
items.append(sub)
|
|
elif toks[i] == ("punct", ","):
|
|
i += 1
|
|
else:
|
|
items.append(toks[i][1] if toks[i][0] != "str" else ("s", toks[i][1]))
|
|
i += 1
|
|
return items, i + 1
|
|
|
|
|
|
def read_module(path):
|
|
"""Yields (word, case name, input, should_error) for every implemented case."""
|
|
toks = tokens(open(path, encoding="utf-8", errors="replace").read())
|
|
for i, t in enumerate(toks):
|
|
if t != ("word", "WordTestSuite"):
|
|
continue
|
|
j = i
|
|
while j < len(toks) and toks[j] != ("punct", "=") and toks[j] != ("punct", ";") and toks[j] != ("punct", "("):
|
|
j += 1
|
|
if j >= len(toks) or toks[j] != ("punct", "="):
|
|
continue
|
|
suites, _ = parse_braces(toks, j + 1)
|
|
for suite in suites:
|
|
if not isinstance(suite, list) or not suite or not isinstance(suite[0], tuple):
|
|
continue
|
|
word = suite[0][1]
|
|
for case in suite[1]:
|
|
if not isinstance(case, list) or len(case) < 6 or not isinstance(case[0], tuple):
|
|
continue
|
|
name, text = case[0][1], case[1][1]
|
|
should_error, implemented = case[4] != "0", case[5] != "0"
|
|
if implemented:
|
|
yield word, name, text, should_error
|
|
|
|
|
|
# ---- cutting a case's line to block lines ------------------------------------
|
|
|
|
STRING_OPENERS = {'."', 'S"', 'ABORT"', '.('}
|
|
|
|
|
|
def atoms(text):
|
|
"""The line as pieces that must each stay on one line: a word, or a word
|
|
that takes text together with its text. None if the line has a new-line
|
|
inside such a text."""
|
|
out, i, n = [], 0, len(text)
|
|
while i < n:
|
|
if text[i].isspace():
|
|
i += 1
|
|
continue
|
|
j = i
|
|
while j < n and not text[j].isspace():
|
|
j += 1
|
|
word = text[i:j]
|
|
upper = word.upper()
|
|
if upper in STRING_OPENERS or upper == "(":
|
|
close = ")" if upper in ("(", ".(") else '"'
|
|
k = text.find(close, j + 1 if j < n else j)
|
|
if k < 0:
|
|
k = n - 1
|
|
piece = text[i:k + 1]
|
|
if "\n" in piece:
|
|
return None
|
|
out.append(piece)
|
|
i = k + 1
|
|
elif word == "\\":
|
|
break # the rest of the line is a comment
|
|
else:
|
|
out.append(word)
|
|
i = j
|
|
return out
|
|
|
|
|
|
def pack(prefix, pieces, width=LINE_MAX):
|
|
"""Pieces into lines no longer than `width`, each begun with `prefix`
|
|
(which may be empty). None if a piece cannot fit on a line."""
|
|
head = [prefix] if prefix else []
|
|
lines, current = [], []
|
|
for piece in pieces:
|
|
if len(" ".join(head + [piece])) > width:
|
|
return None
|
|
if current and len(" ".join(head + current + [piece])) > width:
|
|
lines.append(current)
|
|
current = []
|
|
current.append(piece)
|
|
if current:
|
|
lines.append(current)
|
|
return [" ".join(head + line) for line in lines]
|
|
|
|
|
|
# A store to the search order, which v3's C test runner survives and a
|
|
# running FORTH system does not.
|
|
DAMAGES = re.compile(r"\b(CONTEXT|CURRENT)\s+!")
|
|
|
|
|
|
def forgets_system_word(text):
|
|
"""True if the line FORGETs a word it did not itself define. In a plain
|
|
v3 session that takes the rest of the system with it, and v4 refuses."""
|
|
words = text.split()
|
|
for i, word in enumerate(words[:-1]):
|
|
if word.upper() == "FORGET":
|
|
target = words[i + 1]
|
|
made = any(words[k] == target and k > 0 and words[k - 1].upper() in
|
|
(":", "CREATE", "VARIABLE", "CONSTANT", "VOCABULARY") for k in range(i))
|
|
if not made:
|
|
return True
|
|
return False
|
|
|
|
|
|
# ---- what v3 does --------------------------------------------------------------
|
|
|
|
ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
|
|
def run_v3(v3, cases):
|
|
"""Runs the cases in one session. Returns a list, one entry per case:
|
|
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 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").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
|
|
b = text.find(begin)
|
|
e = text.find(mid, b) if b >= 0 else -1
|
|
s = text.find(end, e) if e >= 0 else -1
|
|
if b < 0 or e < 0 or s < 0:
|
|
results.append(None)
|
|
continue
|
|
body = text[b + len(begin):e]
|
|
# one "ok> " and one " ok" or " ERROR" for every line of the case
|
|
chunks = body.split("ok> ")
|
|
output, error, ok = "", False, True
|
|
for chunk in chunks:
|
|
if chunk.endswith(" ERROR\n"):
|
|
output += chunk[:-len(" ERROR\n")]
|
|
error = True
|
|
elif chunk.endswith(" ok\n"):
|
|
output += chunk[:-len(" ok\n")]
|
|
else:
|
|
ok = False
|
|
m = re.match(r"<(\d+)>((?: -?\d+)*) \n", text[e + len(mid):s])
|
|
if not ok or not m:
|
|
results.append(None)
|
|
continue
|
|
stack = [int(v) for v in m.group(2).split()]
|
|
results.append((error, stack[:-1], output)) # the last value is BASE
|
|
return results
|
|
|
|
|
|
def checksum(output):
|
|
total = 0
|
|
for ch in output.encode("latin-1", errors="replace"):
|
|
total = (total * 31 + ch) & 0xFFFFFF
|
|
return total
|
|
|
|
|
|
# ---- writing the capsule -------------------------------------------------------
|
|
|
|
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), [], [], {}
|
|
for module in MODULES:
|
|
for word, name, text, should_error in read_module(os.path.join(MODULE_DIR, module)):
|
|
if word not in required:
|
|
continue
|
|
label = ("%s.%s" % (word, name))[:31]
|
|
seen[label] = seen.get(label, 0) + 1
|
|
if seen[label] > 1:
|
|
label = (label[:28] + "~%d" % seen[label])[:31]
|
|
if word in KEYBOARD:
|
|
left_out.append((label, "reads the keyboard"))
|
|
continue
|
|
if forgets_system_word(text):
|
|
left_out.append((label, "forgets a word of the system"))
|
|
continue
|
|
if DAMAGES.search(text.upper()):
|
|
left_out.append((label, "damages the system on purpose"))
|
|
continue
|
|
pieces = atoms(text)
|
|
lines = pack("T|", pieces) if pieces is not None else None
|
|
if not lines:
|
|
left_out.append((label, "a line cannot be cut to fit a block line"))
|
|
continue
|
|
cases.append({"label": label, "word": word, "text": text, "lines": lines, "should_error": should_error})
|
|
|
|
# v3 is run until every case left is one it gets through
|
|
while True:
|
|
results = run_v3(args.v3, cases)
|
|
bad = [i for i, r in enumerate(results) if r is None]
|
|
if not bad:
|
|
break
|
|
first = bad[0]
|
|
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()
|
|
|
|
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((label, "v3's table says %s, v3 %s"
|
|
% ("error" if case["should_error"] else "no error",
|
|
"raised one" if error else "raised none")))
|
|
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:
|
|
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"])
|
|
|
|
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")
|
|
|
|
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),
|
|
" 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)]
|
|
report += [" %-32s %s" % item for item in left_out]
|
|
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()
|