Files
LithosAnanake/v4/tools/post79_rules.py
T
rajamesandClaude Opus 5.5 4bdb210a99 feat(v4.0.0): POST is the kernel's -- the boot feeds the cases, and nothing of the harness is on the node
The boot runs POST with the runner (v4/system/post.c) after the capsules:
it feeds the 538 cases to the node and judges them from outside.  A
case's printing no longer reaches the console.  What the cases define
stays in the dictionary, as in v3; the system is sealed after POST and
the boot prints PARITY:V4_SYSTEM word_count=N dict_hash=..., as v3 prints
its parity after POST.

Gone: capsules/v4/post79.4th and its blocks 7000 up; the harness words;
(CATCH) and (EMIT-HOOK), with what EMIT and the prompt loop did for them.

The generator runs v3 on the lines as the kernel sends them, without the
capsule's "T| ".  One expected result follows from that: >IN.initial
prints 6, not 9.

make -C v4 test, sanitize and hosted-check pass; amd64, aarch64 and
riscv64 boot, POST 538 of 538, word_count=411, dict_hash
0x6fb1d09418b189ee on all six: logs/20261007-105118, -105335, -105651.
T{ is unknown at the prompt; RS1 prints 42 42 before and after COLD.  A
scratch build with one expectation changed names the case and ends
PARITY:FAIL, POST: FAILED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:59:02 -04:00

199 lines
12 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
# see OPEN below: HERE handed to a word that takes a byte address
"C!.basic": None,
"C!.zero": None,
"C!.high_byte": None,
"C!.truncation": None,
"C@.after_cstore": None,
"C@.zero_byte": None,
"FILL.basic": None,
"FILL.zero_byte": None,
"CMOVE.overlap": None,
"TYPE.single_char": None,
"COUNT.string_bounds": None,
"COUNT.zero_addr_plus_one": None,
"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),
# 6: the line is ">IN @ . CR" and >IN is read when "@" has been taken with the blank after it. It was 9
# while the cases were a capsule, whose lines each began "T| " (until MESH.md step 6b).
">IN.initial": dict(out="6 \n", why="FORTH-79: >IN is the offset into the input, 6 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),
}
# Found 2026-10-06 (docs/v4.0.0/MESH.md section 10, step 4): on v4 HERE is a
# cell address and C! C@ FILL CMOVE COUNT TYPE take byte addresses, so these
# cases wrote into, or read from, the nucleus's own code at cell HERE/4 --
# and passed, reading back what they had written there. Ruled: left out
# until how HERE and the byte words agree is settled, as its own step.
HERE_BYTE = ("OPEN: HERE is a cell address on v4 and this word takes a byte address (D-1): "
"the case wrote to or read from the nucleus's code at cell HERE/4")
# 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)",
"C!.basic": HERE_BYTE,
"C!.zero": HERE_BYTE,
"C!.high_byte": HERE_BYTE,
"C!.truncation": HERE_BYTE,
"C@.after_cstore": HERE_BYTE,
"C@.zero_byte": HERE_BYTE,
"FILL.basic": HERE_BYTE,
"FILL.zero_byte": HERE_BYTE,
"CMOVE.overlap": HERE_BYTE,
"TYPE.single_char": HERE_BYTE,
"COUNT.string_bounds": HERE_BYTE,
"COUNT.zero_addr_plus_one": HERE_BYTE,
"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"),
]