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>
164 lines
11 KiB
Python
164 lines
11 KiB
Python
"""post79_rules.py -- where POST does not take v3's word for it.
|
|
|
|
mkpost.py takes every case from v3 and, by default, what v3 did with it.
|
|
This file holds the exceptions, each with its reason. Ruled by Captain Bob,
|
|
2026-10-05 (docs/v4.0.0/NUCLEUS.md 6.5):
|
|
|
|
ADDRESS the case leaves or prints a memory address, which is not the
|
|
same number on the two machines. "depth+output": the number of
|
|
values left and what was printed are checked, the values are
|
|
not. "depth": only the number of values, because an address is
|
|
printed.
|
|
|
|
STANDARD v3 departs from FORTH-79, or the case is written for v3's
|
|
machine; v4 follows the standard. The entry gives the case as
|
|
POST runs it: `text` if the line had to change, then what it
|
|
must do -- `error`, or `stack` and `out`, or `depth` alone where
|
|
an address is printed. None drops the case. `why` is the
|
|
reason, and is what docs/v4.0.0/POST79.md lists.
|
|
|
|
EXTRA cases written by hand, for required words v3 has no case for.
|
|
"""
|
|
|
|
D1 = "v3's address unit is a byte; a v4 address unit is a cell (D-1)"
|
|
PIC = "FORTH-79: <# # #S #> work on a double number; v3 takes a single"
|
|
RS = "FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt"
|
|
PICK = "FORTH-79: PICK and ROLL count from 1; v3 counts from 0"
|
|
LEAVE = "FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once"
|
|
V3WORD = "v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand"
|
|
V3ERR = "v3 raises an error FORTH-79 does not ask for"
|
|
VOC = "FORTH-79: a word defined in another vocabulary is not found from FORTH; v3 finds it"
|
|
|
|
ADDRESS = {
|
|
"-TRAILING.basic": "depth+output", "-TRAILING.all_spaces": "depth+output", "-TRAILING.no_spaces": "depth+output",
|
|
"HERE.stability": "depth+output", "PAD.stability": "depth+output",
|
|
"UPDATE.basic": "depth+output", "UPDATE.multiple": "depth+output", "SAVE-BUFFERS.dirty_blocks": "depth+output",
|
|
"EMPTY-BUFFERS.after_use": "depth+output", "EMPTY-BUFFERS.dirty_blocks": "depth+output",
|
|
"BUFFER.flush_dirty": "depth+output",
|
|
"BLOCK.basic": "depth", "BUFFER.basic": "depth",
|
|
"VOCABULARY.create_and_switch": "depth", "CONTEXT.basic": "depth", "CONTEXT.initial": "depth",
|
|
"CURRENT.basic": "depth", "CURRENT.after_def": "depth",
|
|
# the standard leaves the layout of a listing to the system
|
|
"LIST.basic": "depth", "SCR.after_list": "depth",
|
|
}
|
|
|
|
STANDARD = {
|
|
"PICK.pick_0": dict(error=True, why=PICK + "; 0 PICK is an error"),
|
|
"PICK.pick_1": dict(stack=[1, 2, 3], out="3 \n", why=PICK),
|
|
"PICK.pick_2": dict(stack=[1, 2, 3], out="2 \n", why=PICK),
|
|
"ROLL.roll_1": dict(out="3 2 1 \n", why=PICK),
|
|
|
|
">R.basic": dict(text=": RS1 42 >R R@ . R> . CR ; RS1", out="42 42 \n", why=RS),
|
|
">R.zero": dict(text=": RS2 0 >R R@ . R> . CR ; RS2", out="0 0 \n", why=RS),
|
|
">R.negative": dict(text=": RS3 -123 >R R@ . R> . CR ; RS3", out="-123 -123 \n", why=RS),
|
|
">R.multiple": dict(text=": RS4 1 2 >R >R R@ . R> . R@ . R> . CR ; RS4", out="1 1 2 2 \n", why=RS),
|
|
"R>.basic": dict(text=": RS5 42 >R R> . CR ; RS5", out="42 \n", why=RS),
|
|
"R>.lifo_order": dict(text=": RS6 1 2 >R >R R> . R> . CR ; RS6", out="1 2 \n", why=RS),
|
|
"R@.basic": dict(text=": RS7 42 >R R@ . R> DROP CR ; RS7", out="42 \n", why=RS),
|
|
"R@.non_destructive": dict(text=": RS8 99 >R R@ R@ = . R> DROP CR ; RS8", out="-1 \n", why=RS),
|
|
|
|
",.basic": dict(text="42 , HERE 1 - @ . CR", out="42 \n", why=D1),
|
|
",.negative": dict(text="-999 , HERE 1 - @ . CR", out="-999 \n", why=D1),
|
|
"+!.basic": dict(out="15 \n", why=V3ERR + ": a store to HERE"),
|
|
"+!.by_zero": dict(out="42 \n", why=V3ERR + ": a store to HERE"),
|
|
"+!.negative": dict(out="7 \n", why=V3ERR + ": a store to HERE"),
|
|
"+!.accumulate": dict(out="6 \n", why=V3ERR + ": a store to HERE"),
|
|
"HERE.after_comma~2": dict(stack=[0], why="the case divides by zero unless HERE moved by one cell; on v3 it had moved by 8"),
|
|
"MOVE.basic": dict(text="65 HERE ! 66 HERE 1+ ! HERE HERE 16 + 2 MOVE HERE 16 + @ . HERE 17 + @ . CR",
|
|
out="65 66 \n", why="FORTH-79: MOVE moves cells; and " + D1),
|
|
"PAD.usable": None, # see OPEN below
|
|
|
|
"BASE.base_store": dict(out="255 \n", why="255 read in base 16 and printed in base 16 is 255; v3 printed 467"),
|
|
|
|
"<#.basic": dict(out="42\n", why="v3 prints 774763251095801167872 for 42 S>D <# #S #>"),
|
|
"<#.empty": dict(text="0 0 <# #> TYPE CR", out="\n", why=PIC),
|
|
"<#.negative": dict(text="-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR", out="-42\n", why=PIC),
|
|
"#.single_digit": dict(text="15 0 <# # #> TYPE CR", out="5\n", why=PIC),
|
|
"#.multiple": dict(text="15 0 <# # # #> TYPE CR", out="15\n", why=PIC),
|
|
"#.zero_pad": dict(text="5 0 <# # # #> TYPE CR", out="05\n", why=PIC),
|
|
"#S.basic": dict(text="42 0 <# #S #> TYPE CR", out="42\n", why=PIC),
|
|
"#S.zero": dict(text="0 0 <# #S #> TYPE CR", out="0\n", why=PIC),
|
|
"#S.large": dict(text="1234567890 0 <# #S #> TYPE CR", out="1234567890\n", why=PIC),
|
|
"SIGN.negative": dict(text="-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR", out="-42\n", why=PIC),
|
|
"SIGN.positive": dict(text="42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR", out="42\n", why=PIC),
|
|
"SIGN.zero": dict(text="0 DUP ABS 0 <# #S ROT SIGN #> TYPE CR", out="0\n", why=PIC),
|
|
"#>.normal": dict(text="42 0 <# #S #> TYPE CR", out="42\n", why=PIC),
|
|
"#>.empty": dict(text="0 0 <# #> TYPE CR", out="\n", why=PIC),
|
|
"#>.stack_effect": dict(text="42 0 <# #S #> SWAP DROP . CR", out="2 \n", why=PIC),
|
|
"HOLD.basic": dict(text="42 0 <# 46 HOLD #S #> TYPE CR", out="42.\n", why=PIC),
|
|
|
|
"COUNT.basic": dict(text="4 PAD C! PAD COUNT . PAD - . CR", out="4 1 \n", why=D1 + "; and S\" is not FORTH-79"),
|
|
"COUNT.empty": dict(text="0 PAD C! PAD COUNT . PAD - . CR", out="0 1 \n", why=D1),
|
|
"COUNT.max_length": dict(text="255 PAD C! PAD COUNT . PAD - . CR", out="255 1 \n", why=D1),
|
|
"COUNT.basic~2": dict(text="4 PAD C! PAD COUNT . PAD - . CR", out="4 1 \n", why=D1),
|
|
"COUNT.empty~2": dict(text="0 PAD C! PAD COUNT . PAD - . CR", out="0 1 \n", why=D1),
|
|
"COUNT.max_length~2": dict(text="255 PAD C! PAD COUNT . PAD - . CR", out="255 1 \n", why=D1),
|
|
"CMOVE.basic": dict(text="65 PAD C! 66 PAD 1+ C! PAD PAD 10 + 2 CMOVE PAD 10 + C@ . PAD 11 + C@ . CR",
|
|
out="65 66 \n", why=D1 + "; and the v3 case leaves a value on the return stack"),
|
|
"CMOVE.bounds": None, # moves 1000 bytes over the system; v3 refuses by a bounds rule FORTH-79 has not got
|
|
"TYPE.basic_string": dict(text="72 PAD C! 105 PAD 1+ C! PAD 2 TYPE CR", out="Hi\n", why=D1 + "; and " + RS),
|
|
"TYPE.numbers": dict(text="49 PAD C! 50 PAD 1+ C! PAD 2 TYPE CR", out="12\n", why=D1 + "; and " + RS),
|
|
|
|
">IN.initial": dict(out="9 \n", why="FORTH-79: >IN is the offset into the input, 9 here; v3 prints an address"),
|
|
|
|
"WORD.empty_input": None, "WORD.space_delim": None, "WORD.newline_delim": None, "WORD.tab_delim": None,
|
|
"WORD.comma_delim": None, "WORD.skip_leading": None, "WORD.single_char": None, "WORD.long_word": None,
|
|
"WORD.zero_delim": None, "WORD.high_ascii": None, "WORD.count_format": None, "WORD.count_value": None,
|
|
"WORD.multi_delim": None,
|
|
|
|
"UPDATE.no_block": dict(why=V3ERR),
|
|
"SCR.after_load": dict(out="1 \n", why="v3 prints nothing after 1 LOAD; SCR is still 1 from the LIST before"),
|
|
|
|
"FIND.existing": dict(depth=0, why="FORTH-79: FIND takes the next word, DUP here, and leaves its address, which is printed"),
|
|
"FIND.user_word": dict(depth=0, why="the address FIND leaves is printed"),
|
|
"FIND.empty": dict(stack=[0], why="FORTH-79: FIND leaves 0 when there is no word; v3 raises an error"),
|
|
|
|
"VOCABULARY.cross_vocab_access": dict(error=True, why=VOC),
|
|
"VOCABULARY.duplicate": dict(why=V3ERR + ": defining a name again"),
|
|
|
|
"IF.true": dict(out="42 \n", why="v3 runs an older TEST1 from another vocabulary and leaves the flag"),
|
|
"IF.false": dict(out="24 \n", why="v3 runs an older TEST2 from another vocabulary and leaves the flag"),
|
|
"LEAVE.basic": dict(out="4 \n", why=LEAVE),
|
|
"LEAVE.at_start": dict(out="1 \n", why=LEAVE),
|
|
"LEAVE.qdloop": dict(out="3 \n", why=LEAVE),
|
|
}
|
|
|
|
# Dropped, and why. mkpost.py prints these with the cases it leaves out.
|
|
DROPPED = {
|
|
"PAD.usable": "OPEN: PAD 42 OVER ! faults on v4 -- PAD is a byte address and ! takes a cell address (D-1)",
|
|
"CMOVE.bounds": "moves 1000 bytes over the system; v3 refuses by a bounds rule FORTH-79 has not got",
|
|
}
|
|
for _name in [k for k, v in STANDARD.items() if v is None and k.startswith("WORD.")]:
|
|
DROPPED[_name] = V3WORD
|
|
|
|
EXTRA = [
|
|
dict(label="U*.small", text="3 4 U* . . CR", out="0 12 \n"),
|
|
dict(label="U*.zero", text="0 5 U* . . CR", out="0 0 \n"),
|
|
dict(label="U*.full", text="-1 -1 U* 2+ . 1 = . CR", out="0 -1 \n"), # (2^N-1)^2: high cell 2^N-2, low cell 1
|
|
dict(label="U/MOD.small", text="7 0 2 U/MOD . . CR", out="3 1 \n"),
|
|
dict(label="U/MOD.exact", text="12 0 4 U/MOD . . CR", out="3 0 \n"),
|
|
dict(label="U/MOD.double", text="0 1 2 U/MOD 2* . . CR", out="0 0 \n"), # 2^N / 2: quotient 2^(N-1), remainder 0
|
|
dict(label="U/MOD.inverse", text="-1 -1 U* -1 U/MOD 1+ . . CR", out="0 0 \n"),
|
|
dict(label="U/MOD.carry", text="-1 -2 -1 U/MOD 1+ . 2+ . CR", out="0 0 \n"),
|
|
dict(label="U/MOD.by_zero", text="1 0 0 U/MOD", error=True),
|
|
dict(label="?.basic", text="VARIABLE Q1 42 Q1 ! Q1 ? CR", out="42 \n"),
|
|
dict(label="EXECUTE.found", text=": E1 7 ; FIND E1 EXECUTE . CR", out="7 \n"),
|
|
dict(label="U..small", text="42 U. CR", out="42 \n"),
|
|
dict(label="U..unsigned", text="-1 U. CR", out="18446744073709551615 \n"),
|
|
dict(label="CONVERT.digits", text=": C1 0 0 BL WORD CONVERT DROP ; C1 123 . . CR", out="0 123 \n"),
|
|
dict(label="'.found", text="' DUP 0= . CR", out="0 \n"),
|
|
dict(label='.".in_definition', text=': Q3 ." hi there" ; Q3 CR', out="hi there\n"),
|
|
dict(label="LITERAL.basic", text=": Q4 [ 5 ] LITERAL ; Q4 . CR", out="5 \n"),
|
|
dict(label="STATE.interpreting", text="STATE @ . CR", out="0 \n"),
|
|
dict(label="STATE.compiling", text=": Q5 STATE @ 0= . ; IMMEDIATE : Q6 Q5 ; CR", out="0 \n"),
|
|
dict(label="COMPILE.basic", text=": Q7 COMPILE DUP ; IMMEDIATE : Q8 Q7 ; 3 Q8 . . CR", out="3 3 \n"),
|
|
dict(label="[COMPILE].basic", text=": Q9 [COMPILE] IF ; IMMEDIATE : Q10 Q9 1 ELSE 2 THEN ; 0 Q10 . -1 Q10 . CR", out="2 1 \n"),
|
|
dict(label="(.comment", text="1 ( 2 ) 3 . . CR", out="3 1 \n"),
|
|
dict(label="BLK.terminal", text="BLK @ . CR", out="0 \n"),
|
|
dict(label="79-STANDARD.present", text="79-STANDARD"),
|
|
dict(label="WORD.next_word", text=": W1 BL WORD COUNT TYPE ; W1 HELLO", out="HELLO"),
|
|
dict(label="WORD.delimiter", text=": W2 44 WORD COUNT TYPE ; W2 A B,", out="A B"),
|
|
dict(label="WORD.count", text=": W3 BL WORD C@ . ; W3 ABC", out="3 "),
|
|
dict(label="WORD.skips_leading", text=": W4 BL WORD COUNT TYPE ; W4 X", out="X"),
|
|
]
|