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:
rajames
2026-10-05 16:28:01 -04:00
co-authored by Claude Opus 5.5
parent 5d6043e37e
commit 2930349bbf
13 changed files with 1331 additions and 416 deletions
+70 -68
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-10-05T20:03:22Z -->
<!-- Generated by mkcapsule --manifest 2026-10-05T20:24:47Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
@@ -29,8 +29,8 @@
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | n/a |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | n/a |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | n/a |
| `v4:forth79.4th` | 6000 | `0xa4b74bdc7deaf204` | n/a |
| `v4:post79.4th` | 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 7036, 7037, 7038, 7039, 7040, 7041, 7042, 7043, 7044, 7045, 7046, 7047, 7048, 7049, 7050, 7051, 7052, 7053, 7054, 7055, 7056, 7057, 7058, 7059, 7060, 7061, 7062, 7063 | `0x24df7b11992128ed` | n/a |
| `v4:forth79.4th` | 6000, 6001, 6002 | `0x4055641ee17d176b` | n/a |
| `v4:post79.4th` | 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 7036, 7037, 7038, 7039, 7040, 7041, 7042, 7043, 7044, 7045, 7046, 7047, 7048, 7049, 7050, 7051, 7052, 7053, 7054, 7055, 7056, 7057, 7058, 7059, 7060, 7061, 7062, 7063 | `0xb987dbb4388990bd` | n/a |
| `workload-0.4th` | 2200, 2201 | `0x93f86f60aeba8feb` | n/a |
| `workload-1-lite.4th` | 5058, 5059 | `0x44a7a7e3176dcc8d` | n/a |
| `workload-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | n/a |
@@ -368,71 +368,73 @@
| 5113 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5114 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 5115 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok |
| 6000 | `v4:forth79.4th` | `0xa4b74bdc7deaf204` | ok |
| 7000 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7001 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7002 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7003 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7004 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7005 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7006 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7007 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7008 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7009 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7010 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7011 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7012 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7013 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7014 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7015 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7016 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7017 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7018 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7019 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7020 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7021 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7022 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7023 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7024 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7025 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7026 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7027 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7028 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7029 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7030 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7031 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7032 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7033 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7034 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7035 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7036 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7037 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7038 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7039 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7040 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7041 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7042 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7043 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7044 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7045 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7046 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7047 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7048 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7049 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7050 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7051 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7052 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7053 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7054 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7055 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7056 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7057 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7058 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7059 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7060 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7061 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7062 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 7063 | `v4:post79.4th` | `0x24df7b11992128ed` | ok |
| 6000 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 6001 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 6002 | `v4:forth79.4th` | `0x4055641ee17d176b` | ok |
| 7000 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7001 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7002 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7003 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7004 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7005 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7006 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7007 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7008 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7009 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7010 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7011 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7012 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7013 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7014 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7015 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7016 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7017 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7018 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7019 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7020 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7021 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7022 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7023 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7024 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7025 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7026 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7027 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7028 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7029 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7030 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7031 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7032 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7033 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7034 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7035 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7036 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7037 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7038 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7039 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7040 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7041 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7042 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7043 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7044 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7045 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7046 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7047 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7048 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7049 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7050 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7051 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7052 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7053 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7054 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7055 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7056 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7057 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7058 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7059 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7060 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7061 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7062 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
| 7063 | `v4:post79.4th` | `0xb987dbb4388990bd` | ok |
## Conflicts
+23 -1
View File
@@ -3,4 +3,26 @@ Block 6000
( as colon definitions, loaded when the system boots. )
( docs/v4.0.0/NUCLEUS.md. Blocks 6000 up. )
( A word moves here from the assembled nucleus, v4/capsule, )
( once its colon definition passes POST. None has moved yet. )
( once its colon definition passes POST. )
Block 6001
( U* U/MOD -- FORTH-79 unsigned multiply and divide. )
( Neither v3 nor the v4 nucleus had them. )
( The first two words count the bits in a cell. )
: (BITS) ( -- n )
0 1 BEGIN DUP WHILE 2* SWAP 1+ SWAP REPEAT DROP ;
(BITS) CONSTANT (NB)
VARIABLE (UD)
( u1 u2 -- ud the unsigned double product )
: U* UM* ;
Block 6002
( ud u1 -- u2 u3 unsigned: u2 the remainder, u3 the quotient )
( One bit a step: shift ud left, and take u1 from its high )
( cell whenever it goes, counting that in the low cell. )
( A zero divisor is error 11, Division by zero. )
: U/MOD
DUP 0= IF DROP DROP DROP 11 NODE-ERROR ! THEN (UD) !
(NB) 0 DO
DUP 0< >R 2* OVER 0< IF 1+ THEN SWAP 2* SWAP
R> IF (UD) @ - SWAP 1+ SWAP
ELSE DUP (UD) @ U< 0= IF (UD) @ - SWAP 1+ SWAP THEN THEN
LOOP SWAP ;
+310 -263
View File
File diff suppressed because it is too large Load Diff
+26 -4
View File
@@ -178,10 +178,32 @@ and names each failing case.
### 6.4 Generating the expected values
A script reads the v3 test modules, keeps the cases for Required Word Set
words, pipes each line through the hosted v3 binary, and writes the
`T{ ... }T` lines. It is a development tool, run when the cases change; its
output, `post79.4th`, is committed and reviewed like any source.
`v4/tools/mkpost.py` reads the v3 test modules, keeps the cases for
Required Word Set words, runs them in one session of the hosted v3 binary,
and writes `capsules/v4/post79.4th`. It is a development tool
(`make -C v4 post79`), run when the cases or the rules change; its output
is committed and reviewed like any source. Every case starts from the same
state on both machines: empty stack, `DECIMAL`, `FORTH DEFINITIONS`.
### 6.5 Where POST does not take v3's word (ruled 2026-10-05)
`v4/tools/post79_rules.py` holds the exceptions, each with its reason, and
`mkpost.py` writes them out as `POST79.md`, which lists every one.
1. **Address-dependent cases.** A case that leaves or prints a memory
address is checked for the number of values it leaves, and for what it
prints unless an address is printed. The values are not compared.
2. **Where v3 departs from FORTH-79.** The expected result is the
standard's. Where v3's line is not valid FORTH-79 (`>R` at the prompt, a
single number given to `<#`), the line is rewritten so that it is.
3. **Cases written for v3's machine.** A v4 address unit is a cell (D-1);
cases that assume v3's byte addresses are rewritten in v4's terms.
4. **Words v3 has no case for** get cases written by hand, with the
standard's results. `U*` and `U/MOD`, which neither v3 nor the v4
nucleus had, are colon definitions in `forth79.4th`.
Not tested: `KEY`, `EXPECT` and `QUERY`, which wait for the keyboard, and
`QUIT`, which returns to the terminal without ` ok`.
## 7. Hashing, signing and parity
+193
View File
@@ -0,0 +1,193 @@
# 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 550 cases. 443 are v3's, with what the hosted v3 binary did as the
expected result. This file lists every other case, and why.
## 1. Rewritten for FORTH-79 (59)
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 |
|---|---|---|---|---|---|
| `PICK.pick_0` | `1 2 3 0 PICK . CR` | stack `1 2 3`, printed `3 \n` | unchanged | an error | FORTH-79: PICK and ROLL count from 1; v3 counts from 0; 0 PICK is an error |
| `PICK.pick_1` | `1 2 3 1 PICK . CR` | stack `1 2 3`, printed `2 \n` | unchanged | stack `1 2 3`, prints `3 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `PICK.pick_2` | `1 2 3 2 PICK . CR` | stack `1 2 3`, printed `1 \n` | unchanged | stack `1 2 3`, prints `2 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `ROLL.roll_1` | `1 2 3 1 ROLL . . . CR` | stack `empty`, printed `1 3 2 \n` | unchanged | stack `empty`, prints `3 2 1 \n` | FORTH-79: PICK and ROLL count from 1; v3 counts from 0 |
| `>R.basic` | `42 >R R@ . R> . CR` | stack `empty`, printed `42 42 \n` | `: RS1 42 >R R@ . R> . CR ; RS1` | stack `empty`, prints `42 42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.zero` | `0 >R R@ . R> . CR` | stack `empty`, printed `0 0 \n` | `: RS2 0 >R R@ . R> . CR ; RS2` | stack `empty`, prints `0 0 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.negative` | `-123 >R R@ . R> . CR` | stack `empty`, printed `-123 -123 \n` | `: RS3 -123 >R R@ . R> . CR ; RS3` | stack `empty`, prints `-123 -123 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `>R.multiple` | `1 2 >R >R R@ . R> . R@ . R> . CR` | stack `empty`, printed `1 1 2 2 \n` | `: RS4 1 2 >R >R R@ . R> . R@ . R> . CR ; RS4` | stack `empty`, prints `1 1 2 2 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R>.basic` | `42 >R R> . CR` | stack `empty`, printed `42 \n` | `: RS5 42 >R R> . CR ; RS5` | stack `empty`, prints `42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R>.lifo_order` | `1 2 >R >R R> . R> . CR` | stack `empty`, printed `1 2 \n` | `: RS6 1 2 >R >R R> . R> . CR ; RS6` | stack `empty`, prints `1 2 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R@.basic` | `42 >R R@ . R> DROP CR` | stack `empty`, printed `42 \n` | `: RS7 42 >R R@ . R> DROP CR ; RS7` | stack `empty`, prints `42 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `R@.non_destructive` | `99 >R R@ R@ = . R> DROP CR` | stack `empty`, printed `-1 \n` | `: RS8 99 >R R@ R@ = . R> DROP CR ; RS8` | stack `empty`, prints `-1 \n` | FORTH-79: >R R> R@ are for use inside a definition; v4 refuses them at the prompt |
| `,.basic` | `42 , HERE 8 - @ . CR` | stack `empty`, printed `42 \n` | `42 , HERE 1 - @ . CR` | stack `empty`, prints `42 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `,.negative` | `-999 , HERE 8 - @ . CR` | stack `empty`, printed `-999 \n` | `-999 , HERE 1 - @ . CR` | stack `empty`, prints `-999 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `+!.basic` | `10 HERE ! 5 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `15 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.by_zero` | `42 HERE ! 0 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `42 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.negative` | `10 HERE ! -3 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `7 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `+!.accumulate` | `0 HERE ! 1 HERE +! 2 HERE +! 3 HERE +! HERE @ . CR` | error | unchanged | stack `empty`, prints `6 \n` | v3 raises an error FORTH-79 does not ask for: a store to HERE |
| `HERE.after_comma~2` | `HERE 42 , HERE SWAP - 1 CELLS = 0 SWAP /` | error | unchanged | stack `0`, prints nothing | the case divides by zero unless HERE moved by one cell; on v3 it had moved by 8 |
| `MOVE.basic` | `HERE 65 OVER C! HERE 1+ 66 OVER C! HERE HERE 16 + 2 MOVE HERE 16 + C@ . HERE 17 + C@ . CR` | stack `302 303`, printed `65 66 \n` | `65 HERE ! 66 HERE 1+ ! HERE HERE 16 + 2 MOVE HERE 16 + @ . HERE 17 + @ . CR` | stack `empty`, prints `65 66 \n` | FORTH-79: MOVE moves cells; and v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `BASE.base_store` | `16 BASE ! 255 . CR` | stack `empty`, printed `597 \n` | unchanged | stack `empty`, prints `255 \n` | 255 read in base 16 and printed in base 16 is 255; v3 printed 467 |
| `<#.basic` | `DECIMAL 42 S>D <# #S #> TYPE CR` | stack `empty`, printed `774763251095801167872\n` | unchanged | stack `empty`, prints `42\n` | v3 prints 774763251095801167872 for 42 S>D <# #S #> |
| `<#.empty` | `0 <# #> TYPE CR` | stack `0`, printed `\n` | `0 0 <# #> TYPE CR` | stack `empty`, prints `\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `<#.negative` | `-42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `-42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.single_digit` | `15 <# # #> TYPE CR` | stack `empty`, printed `5\n` | `15 0 <# # #> TYPE CR` | stack `empty`, prints `5\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.multiple` | `15 <# # # #> TYPE CR` | stack `empty`, printed `15\n` | `15 0 <# # # #> TYPE CR` | stack `empty`, prints `15\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#.zero_pad` | `5 <# # 0 # #> TYPE CR` | stack `0`, printed `05\n` | `5 0 <# # # #> TYPE CR` | stack `empty`, prints `05\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.basic` | `42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `42 0 <# #S #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.zero` | `0 <# #S #> TYPE CR` | stack `empty`, printed `0\n` | `0 0 <# #S #> TYPE CR` | stack `empty`, prints `0\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#S.large` | `1234567890 <# #S #> TYPE CR` | stack `empty`, printed `1234567890\n` | `1234567890 0 <# #S #> TYPE CR` | stack `empty`, prints `1234567890\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.negative` | `-42 ABS <# #S SIGN #> TYPE CR` | stack `0`, printed `42\n` | `-42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `-42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.positive` | `42 <# #S SIGN #> TYPE CR` | stack `0`, printed `42\n` | `42 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `SIGN.zero` | `0 <# #S SIGN #> TYPE CR` | stack `0`, printed `0\n` | `0 DUP ABS 0 <# #S ROT SIGN #> TYPE CR` | stack `empty`, prints `0\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.normal` | `42 <# #S #> TYPE CR` | stack `empty`, printed `42\n` | `42 0 <# #S #> TYPE CR` | stack `empty`, prints `42\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.empty` | `0 <# #> TYPE CR` | stack `0`, printed `\n` | `0 0 <# #> TYPE CR` | stack `empty`, prints `\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `#>.stack_effect` | `42 <# #S #> SWAP . . CR` | stack `empty`, printed `24 2 \n` | `42 0 <# #S #> SWAP DROP . CR` | stack `empty`, prints `2 \n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `HOLD.basic` | `42 <# 46 HOLD #S #> TYPE CR` | stack `empty`, printed `42.\n` | `42 0 <# 46 HOLD #S #> TYPE CR` | stack `empty`, prints `42.\n` | FORTH-79: <# # #S #> work on a double number; v3 takes a single |
| `COUNT.basic` | `HERE S" Test" DROP COUNT . . CR` | stack `302`, printed `84 303 \n` | `4 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `4 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1); and S" is not FORTH-79 |
| `COUNT.empty` | `HERE 0 OVER C! COUNT . . CR` | stack `empty`, printed `0 308 \n` | `0 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `0 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.max_length` | `HERE 255 OVER C! COUNT . . CR` | stack `empty`, printed `255 308 \n` | `255 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `255 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.basic~2` | `HERE S" Test" DROP C@ HERE 1+ SWAP COUNT . . CR` | stack `412 418`, printed `0 85 \n` | `4 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `4 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.empty~2` | `HERE 0 OVER C! COUNT . . CR` | stack `empty`, printed `0 418 \n` | `0 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `0 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `COUNT.max_length~2` | `HERE 255 OVER C! COUNT . . CR` | stack `empty`, printed `255 418 \n` | `255 PAD C! PAD COUNT . PAD - . CR` | stack `empty`, prints `255 1 \n` | v3's address unit is a byte; a v4 address unit is a cell (D-1) |
| `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 `852597488 \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 `852596848 \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 `852923888 \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 |
| `IF.true` | `: TEST1 IF 42 ELSE 24 THEN ; -1 TEST1 . CR` | stack `empty`, printed `42 \n` | unchanged | stack `empty`, prints `42 \n` | v3 runs an older TEST1 from another vocabulary and leaves the flag |
| `IF.false` | `: TEST2 IF 42 ELSE 24 THEN ; 0 TEST2 . CR` | stack `empty`, printed `24 \n` | unchanged | stack `empty`, prints `24 \n` | v3 runs an older TEST2 from another vocabulary and leaves the flag |
| `LEAVE.basic` | `: TLV 0 5 0 DO I 3 = IF LEAVE THEN 1+ LOOP ; TLV . CR` | stack `empty`, printed `3 \n` | unchanged | stack `empty`, prints `4 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
| `LEAVE.at_start` | `: TLV2 0 5 0 DO LEAVE 1+ LOOP ; TLV2 . CR` | stack `empty`, printed `0 \n` | unchanged | stack `empty`, prints `1 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
| `LEAVE.qdloop` | `: TLV3 0 5 0 ?DO I 2 = IF LEAVE THEN 1+ LOOP ; TLV3 . CR` | stack `empty`, printed `2 \n` | unchanged | stack `empty`, prints `3 \n` | FORTH-79: LEAVE ends the loop at the next LOOP; v3 jumps out at once |
## 2. Address-dependent (20)
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 |
|---|---|---|
| `-TRAILING.basic` | `HERE S" Test " -TRAILING TYPE CR` | values left, and output |
| `-TRAILING.all_spaces` | `HERE S" " -TRAILING TYPE CR` | values left, and output |
| `-TRAILING.no_spaces` | `HERE S" Test" -TRAILING TYPE CR` | values left, and output |
| `HERE.stability` | `HERE DUP HERE = . CR` | values left, and output |
| `PAD.stability` | `PAD DUP PAD = . CR` | values left, and output |
| `UPDATE.basic` | `1 BLOCK UPDATE` | values left, and output |
| `UPDATE.multiple` | `1 BLOCK UPDATE UPDATE` | values left, and output |
| `SAVE-BUFFERS.dirty_blocks` | `1 BLOCK UPDATE SAVE-BUFFERS` | values left, and output |
| `EMPTY-BUFFERS.after_use` | `1 BLOCK EMPTY-BUFFERS` | values left, and output |
| `EMPTY-BUFFERS.dirty_blocks` | `1 BLOCK UPDATE EMPTY-BUFFERS` | values left, and output |
| `BUFFER.flush_dirty` | `2 BLOCK 1+ 2 BUFFER` | values left, and output |
| `BLOCK.basic` | `1 BLOCK DUP . CR` | values left only |
| `BUFFER.basic` | `1 BUFFER DUP . CR` | values left only |
| `VOCABULARY.create_and_switch` | `VOCABULARY MYVOC MYVOC DEFINITIONS CONTEXT @ . CR` | values left only |
| `CONTEXT.basic` | `CONTEXT @ . CR` | values left only |
| `CONTEXT.initial` | `FORTH CONTEXT @ . CR` | values left only |
| `CURRENT.basic` | `CURRENT @ . CR` | values left only |
| `CURRENT.after_def` | `VOCABULARY TEST-VOC8 TEST-VOC8 DEFINITIONS CURRENT @ . CR` | values left only |
| `LIST.basic` | `1 LIST` | values left only |
| `SCR.after_list` | `1 LIST SCR @ . CR` | values left only |
## 3. Written by hand (28)
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 |
|---|---|---|
| `U*.small` | `3 4 U* . . CR` | stack `empty`, prints `0 12 \n` |
| `U*.zero` | `0 5 U* . . CR` | stack `empty`, prints `0 0 \n` |
| `U*.full` | `-1 -1 U* 2+ . 1 = . CR` | stack `empty`, prints `0 -1 \n` |
| `U/MOD.small` | `7 0 2 U/MOD . . CR` | stack `empty`, prints `3 1 \n` |
| `U/MOD.exact` | `12 0 4 U/MOD . . CR` | stack `empty`, prints `3 0 \n` |
| `U/MOD.double` | `0 1 2 U/MOD 2* . . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.inverse` | `-1 -1 U* -1 U/MOD 1+ . . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.carry` | `-1 -2 -1 U/MOD 1+ . 2+ . CR` | stack `empty`, prints `0 0 \n` |
| `U/MOD.by_zero` | `1 0 0 U/MOD` | an error |
| `?.basic` | `VARIABLE Q1 42 Q1 ! Q1 ? CR` | stack `empty`, prints `42 \n` |
| `EXECUTE.found` | `: E1 7 ; FIND E1 EXECUTE . CR` | stack `empty`, prints `7 \n` |
| `U..small` | `42 U. CR` | stack `empty`, prints `42 \n` |
| `U..unsigned` | `-1 U. CR` | stack `empty`, prints `18446744073709551615 \n` |
| `CONVERT.digits` | `: C1 0 0 BL WORD CONVERT DROP ; C1 123 . . CR` | stack `empty`, prints `0 123 \n` |
| `'.found` | `' DUP 0= . CR` | stack `empty`, prints `0 \n` |
| `.".in_definition` | `: Q3 ." hi there" ; Q3 CR` | stack `empty`, prints `hi there\n` |
| `LITERAL.basic` | `: Q4 [ 5 ] LITERAL ; Q4 . CR` | stack `empty`, prints `5 \n` |
| `STATE.interpreting` | `STATE @ . CR` | stack `empty`, prints `0 \n` |
| `STATE.compiling` | `: Q5 STATE @ 0= . ; IMMEDIATE : Q6 Q5 ; CR` | stack `empty`, prints `0 \n` |
| `COMPILE.basic` | `: Q7 COMPILE DUP ; IMMEDIATE : Q8 Q7 ; 3 Q8 . . CR` | stack `empty`, prints `3 3 \n` |
| `[COMPILE].basic` | `: Q9 [COMPILE] IF ; IMMEDIATE : Q10 Q9 1 ELSE 2 THEN ; 0 Q10 . -1 Q10 . CR` | stack `empty`, prints `2 1 \n` |
| `(.comment` | `1 ( 2 ) 3 . . CR` | stack `empty`, prints `3 1 \n` |
| `BLK.terminal` | `BLK @ . CR` | stack `empty`, prints `0 \n` |
| `79-STANDARD.present` | `79-STANDARD` | stack `empty`, prints nothing |
| `WORD.next_word` | `: W1 BL WORD COUNT TYPE ; W1 HELLO` | stack `empty`, prints `HELLO` |
| `WORD.delimiter` | `: W2 44 WORD COUNT TYPE ; W2 A B,` | stack `empty`, prints `A B` |
| `WORD.count` | `: W3 BL WORD C@ . ; W3 ABC` | stack `empty`, prints `3 ` |
| `WORD.skips_leading` | `: W4 BL WORD COUNT TYPE ; W4 X` | stack `empty`, prints `X` |
## 4. v3 cases left out (27)
| Case | Why |
|---|---|
| `EXPECT.zero_length` | reads the keyboard |
| `EXPECT.one_item` | reads the keyboard |
| `EXPECT.empty_stack` | reads the keyboard |
| `FORGET.nonexistent` | forgets a word of the system |
| `FORGET.protected` | forgets a word of the system |
| `CONTEXT.modify` | damages the system on purpose |
| `CURRENT.protect` | damages the system on purpose |
| `QUIT.in_definition` | v3 did not get through it in one session |
| `:.nested` | v3 did not get through it in one session |
| `].basic` | v3 did not get through it in one session |
| `ELSE.double` | v3 did not get through it in one session |
| `THEN.extra` | v3 did not get through it in one session |
| `CMOVE.bounds` | moves 1000 bytes over the system; v3 refuses by a bounds rule FORTH-79 has not got |
| `WORD.empty_input` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.space_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.newline_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.tab_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.comma_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.skip_leading` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.single_char` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.long_word` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.zero_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.high_ascii` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.count_format` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.count_value` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `WORD.multi_delim` | v3's WORD does not take the next word from the input, as FORTH-79's does; see the WORD cases written by hand |
| `PAD.usable` | OPEN: PAD 42 OVER ! faults on v4 -- PAD is a byte address and ! takes a cell address (D-1) |
## 5. Required words POST does not test
`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.
## 6. Where v3 did not do what its own table expects (6)
POST expects what v3 did, unless section 1 says otherwise.
| Case | |
|---|---|
| `+!.basic` | v3's table says no error, v3 raised one |
| `+!.by_zero` | v3's table says no error, v3 raised one |
| `+!.negative` | v3's table says no error, v3 raised one |
| `+!.accumulate` | v3's table says no error, v3 raised one |
| `HERE.after_comma~2` | v3's table says no error, v3 raised one |
| `CREATE.long_name` | v3's table says error, v3 raised none |
@@ -0,0 +1,112 @@
[=3hBdsDxe: loading Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
BdsDxe: starting Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
[=3hStarKernel UEFI Loader
Loading kernel from ESP...
[CKPT 001] Entered efi_main - ConOut live
RAW SERIAL UP
[CKPT 002] Serial (COM1) initialized
Monolithic build - kernel linked directly
Collecting boot information...
CmdLine: parsed OK
[CKPT 004] Command line parsed
[CKPT 005] Kernel stack allocation decided
[CKPT 006] Boot info collected (ACPI table located)
GOP: linear framebuffer found
[CKPT 007] GOP: linear framebuffer found
[CKPT 008] About to enter ExitBootServices retry loop
EBS...
EBS OK
Calling kernel_main (monolithic)...
_____ _ _ __ _
/ ____| | | |/ / | |
| (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
\___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
____) | || (_| | | | . \ __/ | | | | | __/ |
|_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
LithosAnanke v2.1.0
Architecture: amd64
Build: Oct 5 2026 16:18:59
UEFI BootServices: EXITED
=== StarKernel Boot Information ===
Memory map entries: 135
Total memory: 1023 MB
Usable memory: 968 MB
===================================
PMM initialized.
PMM statistics:
Total pages: 249248
Free pages : 247725
Used pages : 1523
Total MB : 973
Free MB : 967
Used MB : 5
VMM initialized (mapped RAM, CR3 switched)
VMM initialized (mapped RAM, CR3 switched)
VMM self-test: mapped OK at 0xffff800000000000
VMM self-test complete.
IDT installed.
APIC: init...
APIC: IA32_APIC_BASE MSR=0x00000000fee00900
APIC: stale-ISR drain: 0 EOI(s) issued
APIC: initialized (xAPIC MMIO, TPR=0, SIVR=0x1FF, EOI-clear)
APIC: init done
I/O APIC: init...
I/O APIC: base=0xfec00000, gsi_base=0, overrides=5
override: source=00 gsi=2 flags=0x0000
override: source=05 gsi=5 flags=0x000d
override: source=09 gsi=9 flags=0x000d
override: source=0a gsi=10 flags=0x000d
override: source=0b gsi=11 flags=0x000d
i8042: keyboard ACKed enable-scanning
i8042: IRQ1 enabled in controller config byte
I/O APIC: keyboard IRQ1 routed (masked)
Timer: init...
Timer: init start
Timer: PM_TMR_BLK discovered from FADT at port 1544
Timer: VM mode detected (hypervisor present).
Timer: HPET calibration disabled (VM-exit MMIO would poison timing).
Timer: WARNING: invariant TSC not present under hypervisor.
Timer: continuing in RELATIVE mode (no determinism guarantees).
Timer: RDTSCP not present; using RDTSC (less serialized).
Timer: CPUID frequency unavailable; trying PM Timer...
Timer: trust=1 (0=NONE,1=REL,2=ABS), TSC=2100864460 Hz
Timer: init done
Kernel heap initialized.
Heap statistics:
Total bytes: 536870872
Free bytes: 536870872
Used bytes: 0
Peak bytes: 0
Heap base addr: 24641536
Heap end addr: 561512448
Heartbeat: init...
APIC Timer: calibrating...
APIC Timer: apic_hz=1006177928, tick_hz=100, initial_count=10061779
APIC Timer: configured (masked, ready to start)
Heartbeat: init done
Kernel initialization complete.
Boot successful!
StarForth v4: one host node, the F18-derived engine
PARITY:V4_NUCLEUS words=295 image_hash=0x3ed5d5fc83f0b026
V4: capsule v4:forth79.4th signature: missing (unsigned)
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x1ec33b48cc76a0b5
V4: capsule v4:post79.4th signature: missing (unsigned)
PARITY:V4_POST tests=550 pass=550 fail=0
PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x671fdd7aebf0f8d8
PARITY:OK
POST: PASSED
ok>
@@ -0,0 +1,86 @@
UEFI firmware (version 2025.11-3ubuntu7.3 built at 15:40:26 on Sep 23 2026)
[=3hBdsDxe: failed to load Boot0002 "UEFI Misc Device" from PciRoot(0x0)/Pci(0x2,0x0): Not Found
BdsDxe: failed to load Boot0003 "UEFI QEMU QEMU USB HARDDRIVE 1-0000:00:04.0-1" from PciRoot(0x0)/Pci(0x4,0x0)/USB(0x0,0x0): Not Found
BdsDxe: loading Boot0004 "UEFI Misc Device 2" from PciRoot(0x0)/Pci(0x6,0x0)
BdsDxe: starting Boot0004 "UEFI Misc Device 2" from PciRoot(0x0)/Pci(0x6,0x0)
[=3hStarKernel UEFI Loader
Loading kernel from ESP...
[CKPT 001] Entered efi_main - ConOut live
Collecting boot information...
[CKPT 004] Command line parsed
[CKPT 005] Kernel stack allocation decided
[CKPT 006] Boot info collected (ACPI table located)
[CKPT 007] GOP: linear framebuffer found
[CKPT 008] About to enter ExitBootServices retry loop
_____ _ _ __ _
/ ____| | | |/ / | |
| (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
\___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
____) | || (_| | | | . \ __/ | | | | | __/ |
|_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
LithosAnanke v2.1.0
Architecture: aarch64
Build: Oct 5 2026 16:21:17
UEFI BootServices: EXITED
=== StarKernel Boot Information ===
Memory map entries: 108
Total memory: 4093 MB
Usable memory: 4056 MB
===================================
PMM initialized.
PMM statistics:
Total pages: 1039860
Free pages : 1038449
Used pages : 1411
Total MB : 4061
Free MB : 4056
Used MB : 5
VMM initialized (mapped RAM, CR3 switched)
VMM initialized (mapped RAM, CR3 switched)
VMM self-test: mapped OK at 0xffff800000000000
VMM self-test complete.
AArch64: running at EL1
IDT installed.
APIC: init...
PSCI: no DTB -- using HVC (QEMU virt-machine default)
GICv2: no DTB GIC node -- using QEMU virt-machine defaults
GICv2: distributor+CPU interface enabled, PPI 30
APIC: init done
Timer: init...
Timer: AArch64 generic timer initialised.
Timer: init done
Kernel heap initialized.
Heap statistics:
Total bytes: 2147483608
Free bytes: 2147483608
Used bytes: 0
Peak bytes: 0
Heap base addr: 1207959552
Heap end addr: 3355443200
Heartbeat: init...
Heartbeat: init done
Kernel initialization complete.
Boot successful!
StarForth v4: one host node, the F18-derived engine
PARITY:V4_NUCLEUS words=295 image_hash=0x3ed5d5fc83f0b026
V4: capsule v4:forth79.4th signature: missing (unsigned)
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x1ec33b48cc76a0b5
V4: capsule v4:post79.4th signature: missing (unsigned)
PARITY:V4_POST tests=550 pass=550 fail=0
PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x671fdd7aebf0f8d8
PARITY:OK
POST: PASSED
ok>
@@ -0,0 +1,156 @@
OpenSBI v1.8
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : riscv-virtio,qemu
Platform Features : medeleg
Platform HART Count : 1
Platform HART Protection : pmp
Platform IPI Device : aclint-mswi
Platform Timer Device : aclint-mtimer @ 10000000Hz
Platform Console Device : uart8250
Platform HSM Device : ---
Platform PMU Device : ---
Platform Reboot Device : syscon-reboot
Platform Shutdown Device : syscon-poweroff
Platform Suspend Device : ---
Platform CPPC Device : ---
Firmware Base : 0x80000000
Firmware Size : 321 KB
Firmware RW Offset : 0x40000
Firmware RW Size : 65 KB
Firmware Heap Offset : 0x47000
Firmware Heap Size : 37 KB (total), 0 KB (reserved), 12 KB (used), 23 KB (free)
Firmware Scratch Size : 4096 B (total), 1464 B (used), 2632 B (free)
Runtime SBI Version : 3.0
Standard SBI Extensions : ipi,pmu,srst,sse,hsm,rfnc,fwft,time,base,legacy,dbcn,dbtr
Experimental SBI Extensions : none
Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080040000-0x000000008005ffff M: (F,R,W) S/U: ()
Domain0 Region01 : 0x0000000080000000-0x000000008003ffff M: (F,R,X) S/U: ()
Domain0 Region02 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W)
Domain0 Region03 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W)
Domain0 Region04 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: ()
Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W)
Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W)
Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X)
Domain0 Next Address : 0x0000000020000000
Domain0 Next Arg1 : 0x00000000bfe00000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes
Domain0 SysSuspend : yes
Boot HART ID : 0
Boot HART Domain : root
Boot HART Priv Version : v1.12
Boot HART Base ISA : rv64imafdch
Boot HART ISA Extensions : sstc,zicntr,zihpm,zicboz,zicbom,sdtrig,svadu
Boot HART PMP Count : 16
Boot HART PMP Granularity : 2 bits
Boot HART PMP Address Bits : 54
Boot HART MHPM Info : 16 (0x0007fff8)
Boot HART Debug Triggers : 2 triggers
Boot HART MIDELEG : 0x0000000000001666
Boot HART MEDELEG : 0x0000000000f4b509
[=3hRISC-V EDK2 firmware version 2025.11-3ubuntu7.3
Press ESCAPE within 5 seconds for boot options ERROR: C40000002:V03051002 I0 6D33944A-EC75-4855-A54D-809C75241F6C 83FFF850
BdsDxe: failed to load Boot0001 "UEFI Misc Device" fr
om PciRoot(0x0)/Pci(0x1,0x0): Not Found
[=3hStarKernel UEFI Loader
Loading kernel from ESP...
[CKPT 001] Entered efi_main - ConOut live
Monolithic build - kernel linked directly
Collecting boot information...
CmdLine: parsed OK
[CKPT 004] Command line parsed
[CKPT 005] Kernel stack allocation decided
[CKPT 006] Boot info collected (ACPI table located)
GOP: linear framebuffer found
[CKPT 007] GOP: linear framebuffer found
[CKPT 008] About to enter ExitBootServices retry loop
Calling kernel_main (monolithic)...
riscv64 item 4.3.5a: satp state at kernel entry (before switch)
satp.MODE = 0x000000000000000a
satp.PPN = 0x00000000000bf868
__kernel_start = 0x00000000bde97936
riscv64: satp cleared -- Bare mode, explicit (item 4.3.5a)
_____ _ _ __ _
/ ____| | | |/ / | |
| (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |
\___ \| __/ _` | '__| < / _ \ '__| '_ \ / _ \ |
____) | || (_| | | | . \ __/ | | | | | __/ |
|_____/ \__\__,_|_| |_|\_\___|_| |_| |_|\___|_|
LithosAnanke v2.1.0
Architecture: riscv64
Build: Oct 5 2026 16:24:48
UEFI BootServices: EXITED
=== StarKernel Boot Information ===
Memory map entries: 100
Total memory: 1020 MB
Usable memory: 977 MB
===================================
PMM initialized.
PMM statistics:
Total pages: 250774
Free pages : 250122
Used pages : 652
Total MB : 979
Free MB : 977
Used MB : 2
VMM initialized (mapped RAM, CR3 switched)
VMM initialized (mapped RAM, CR3 switched)
VMM self-test: mapped OK at 0xffff800000000000
VMM self-test complete.
IDT installed.
APIC: init...
PLIC: no DTB PLIC node -- using QEMU virt-machine default (base=0x0c000000)
PLIC: init (S-mode context 1, threshold=0)
APIC: init done
Timer: init...
Timer: RISC-V time CSR @ 10000000 Hz (FALLBACK, no devicetree)
Timer: init done
Kernel heap initialized.
Heap statistics:
Total bytes: 536870872
Free bytes: 536870872
Used bytes: 0
Peak bytes: 0
Heap base addr: 2214588416
Heap end addr: 2751459328
Heartbeat: init...
Heartbeat: init done
Kernel initialization complete.
Boot successful!
StarForth v4: one host node, the F18-derived engine
PARITY:V4_NUCLEUS words=295 image_hash=0x3ed5d5fc83f0b026
V4: capsule v4:forth79.4th signature: missing (unsigned)
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x1ec33b48cc76a0b5
V4: capsule v4:post79.4th signature: missing (unsigned)
PARITY:V4_POST tests=550 pass=550 fail=0
PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x671fdd7aebf0f8d8
PARITY:OK
POST: PASSED
ok>
+9 -12
View File
@@ -129,27 +129,24 @@ $(BINDIR)/boot-$(1).txt: $(BINDIR)/starforth4-$(1)
endef
$(foreach i,$(HOSTED_ISAS),$(eval $(call HOSTED_RULE,$(i))))
# `make post` builds the amd64 system with POST in its boot
# (V4_POST_AT_BOOT=1, system/boot.c) and boots it: the POST capsule,
# ../capsules/v4/post79.4th, is loaded after forth79.4th and must end with a
# clean tally. `make post79` writes that capsule again from v3's cases
# (tools/mkpost.py); it needs the hosted v3 binary.
# `make post` boots the amd64 system alone, which is the quick check after a
# change to the vocabulary: nucleus, forth79.4th, POST, prompt. `make
# post79` writes the POST capsule, ../capsules/v4/post79.4th, again from
# v3's cases and tools/post79_rules.py (tools/mkpost.py); it needs the
# hosted v3 binary.
.PHONY: post post79
$(BINDIR)/starforth4-post: $(HERE)/tools/hosted.c $(BINDIR)/v4_image_64.c $(CAPSULE_DIR_C) $(ENGINE_SRCS) $(SYSTEM_SRCS) $(wildcard $(HERE)/include/v4/*.h) $(HERE)/Makefile
$(CC_amd64) $(CFLAGS) -D_POSIX_C_SOURCE=200809L -I$(HERE)/include -DV4_CELL_BITS=64 $(HOST_DEFS) -c $(HERE)/tools/hosted.c -o $(BINDIR)/hosted-post.o
$(CC_amd64) $(SYSTEM_CFLAGS) -DV4_POST_AT_BOOT=1 -static $(BINDIR)/hosted-post.o $(BINDIR)/v4_image_64.c $(CAPSULE_DIR_C) $(ENGINE_SRCS) $(SYSTEM_SRCS) -o $@
post: $(BINDIR)/starforth4-post
@$(BINDIR)/starforth4-post < /dev/null
post: $(BINDIR)/starforth4-amd64
@$(BINDIR)/starforth4-amd64 < /dev/null
post79:
python3 $(HERE)/tools/mkpost.py
# `make hosted` builds the three. `make hosted-check` boots each with no
# input and requires that all three print the same lines, ending in
# PARITY:OK and the prompt: the same hashes on every ISA.
# PARITY:OK, POST: PASSED and the prompt: the same hashes on every ISA.
.PHONY: hosted hosted-check
hosted: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/starforth4-$(i))
hosted-check: $(foreach i,$(HOSTED_ISAS),$(BINDIR)/boot-$(i).txt)
@grep -q '^PARITY:OK$$' $(BINDIR)/boot-amd64.txt || { echo "hosted-check: no PARITY:OK"; exit 1; }
@grep -q '^POST: PASSED$$' $(BINDIR)/boot-amd64.txt || { echo "hosted-check: no POST: PASSED"; exit 1; }
@cmp $(BINDIR)/boot-amd64.txt $(BINDIR)/boot-aarch64.txt
@cmp $(BINDIR)/boot-amd64.txt $(BINDIR)/boot-riscv64.txt
@echo "hosted-check: amd64, aarch64 and riscv64 boot identically"
+32 -37
View File
@@ -55,58 +55,53 @@ and fails unless their output is identical and ends in `PARITY:OK`.
A capsule line the node does not answer ` ok` to ends the boot, naming the
capsule, block and line, with `PARITY:FAIL` and `POST: FAILED`.
State, 2026-10-05, after the POST work began: all six builds load
`v4:forth79.4th` and reach `ok>` with the same lines:
State, 2026-10-05: all six builds start the nucleus, load `v4:forth79.4th`,
pass POST and reach `ok>`, with the same lines:
```
PARITY:V4_NUCLEUS words=295 image_hash=0x3ed5d5fc83f0b026
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0xa4b74bdc7deaf204 capsule_hash=0xa4b74bdc7deaf204 dict_hash=0x118de9d37e682ea4
PARITY:V4_CAPSULE name=v4:forth79.4th capsule_id=0x4055641ee17d176b capsule_hash=0x4055641ee17d176b dict_hash=0x1ec33b48cc76a0b5
PARITY:V4_POST tests=550 pass=550 fail=0
PARITY:V4_CAPSULE name=v4:post79.4th capsule_id=0xb987dbb4388990bd capsule_hash=0xb987dbb4388990bd dict_hash=0x671fdd7aebf0f8d8
PARITY:OK
POST: PASSED
ok>
```
Bare-metal logs: `logs/20261005-160144/amd64/`, `logs/20261005-160234/aarch64/`,
`logs/20261005-160400/riscv64/`. Each run was ended once the prompt was in the
Bare-metal logs: `logs/20261005-161924/amd64/`, `logs/20261005-162142/aarch64/`,
`logs/20261005-162524/riscv64/`. Each run was ended once the prompt was in the
log; nothing was typed at a bare-metal prompt, so keyboard input there is not
yet verified. The capsules were unsigned (no signing key on this machine).
`forth79.4th` holds no definitions yet: every word is still in the nucleus.
**What this does not yet show.** `forth79.4th` holds two definitions, `U*`
and `U/MOD`. Every other word is still in the assembled nucleus, so POST is
so far a test of the assembled words. Moving them to the capsule, a group at
a time with POST after each, is the next step (`NUCLEUS.md` section 8).
## POST
`capsules/v4/post79.4th` is v3's POST cases for the FORTH-79 Required Word
Set, written by `v4/tools/mkpost.py` (`make -C v4 post79`): 537 cases in
blocks 7000–7112, covering 111 of the 130 required words. Each case carries
what the hosted v3 binary did with the same line — error or not, the stack,
the length and checksum of what was printed — and v4 must do the same.
`capsules/v4/post79.4th`: 550 cases in blocks 7000 up, covering 126 of the
130 words of the FORTH-79 Required Word Set. 443 are v3's cases with what
the hosted v3 binary did as the expected result. The other 107, and the 27
v3 cases left out, are listed with reasons in `docs/v4.0.0/POST79.md`.
**POST does not pass yet, so it is not in the boot.** `make -C v4 post` builds
the amd64 system with it (`-DV4_POST_AT_BOOT=1`) and boots it:
- `make -C v4 post` boots the amd64 system: the quick check after a change.
- `make -C v4 hosted-check` boots all three hosted ISAs and compares them.
- `make -C v4 post79` writes the capsule again (`tools/mkpost.py`,
`tools/post79_rules.py`); it needs the hosted v3 binary.
```
PARITY:V4_POST tests=537 pass=439 fail=98
PARITY:FAIL
POST: FAILED
```
POST is FORTH: a harness of FORTH-79 words and two nucleus hooks, `(CATCH)`
and `(EMIT-HOOK)`. It forgets itself when it has finished. The boot passes
only on seeing POST's tally line with `fail=0` (`v4/system/boot.c`). A colon
definition broken on purpose (`U/MOD` without its last `SWAP`) fails five
cases and the boot stops with `POST: FAILED`.
The 98 are not yet sorted into v4 defects and differences that need a
ruling. A first reading finds four kinds:
Not tested: `KEY`, `EXPECT`, `QUERY` (the keyboard) and `QUIT` (it returns
without ` ok`).
| Kind | About | Examples |
|---|---|---|
| The case prints or leaves a memory address, which differs between the two machines | 40 | `HERE`, `PAD`, `1 BLOCK`, `BL WORD`, `CONTEXT @ .`, `FIND` |
| v3 departs from FORTH-79 and v4 follows the standard | 35 | `PICK` and `ROLL` count from 1; `<# # #S #>` take a double number; `LEAVE` ends the loop at the next `LOOP`; `>R` `R>` only inside a definition |
| The case assumes v3's byte address units, where a v4 address unit is a cell (D-1) | 8 | `42 , HERE 8 - @`, `PAD 42 OVER !` |
| Not yet understood | 15 | vocabulary cases, `LIST`'s layout, `16 BASE ! 255 .`, `321 EMIT`, cases v3 ends in an error and v4 does not |
Open: `PAD 42 OVER !` faults on v4, because `PAD` is a byte address and `!`
takes a cell address (D-1). FORTH-79 expects it to work. The v3 case for it
is left out until that is ruled.
Required words with no v3 case, and so no POST case: `U*` `U/MOD` `?`
`EXECUTE` `KEY` `EXPECT` `QUERY` `U.` `CONVERT` `'` `."` `LITERAL` `STATE`
`COMPILE` `[COMPILE]` `(` `QUIT` `BLK` `79-STANDARD`. `U*` and `U/MOD` are
in neither v3 nor v4.
Twelve v3 cases are left out: three read the keyboard, two forget a word of
the system, two store to the search order, and five leave v3 unable to
continue in one session (`mkpost.py` prints the list).
The boot passes POST only on seeing the tally line with `fail=0`
(`v4/system/boot.c`); that no line was refused is not enough.
The Required Word Set list in `tools/mkpost.py` was written from memory of
the standard and has not been checked against the document.
+12 -8
View File
@@ -11,11 +11,11 @@
#include "starkernel/capsule_blocks.h"
/* The capsules, in the order they are loaded. */
/* POST is not yet part of the boot: v4:post79.4th does not pass yet
* (v4/README.md). Build with -DV4_POST_AT_BOOT=1 to load it and to require
* its clean tally; `make -C v4 post` does. */
/* POST is part of the boot: v4:post79.4th is loaded after the vocabulary it
* tests and must end with a clean tally. -DV4_POST_AT_BOOT=0 leaves it
* out, for work on a vocabulary that does not pass yet. */
#ifndef V4_POST_AT_BOOT
#define V4_POST_AT_BOOT 0
#define V4_POST_AT_BOOT 1
#endif
static const char *const boot_capsules[] = {
"v4:forth79.4th",
@@ -136,14 +136,18 @@ static int text_at(const char *line, unsigned len, unsigned at, const char *want
static void post_watch(char c)
{
static const char clean[] = " fail=0";
unsigned i;
if (c != '\n') {
if (post_len < sizeof post_line) post_line[post_len++] = c;
return;
}
if (text_at(post_line, post_len, 0, "PARITY:V4_POST ")) {
post_seen = 1;
post_clean = post_len >= sizeof clean - 1 && text_at(post_line, post_len, post_len - (unsigned)(sizeof clean - 1), clean);
}
/* the node's prompt comes before it on the line, so look along the line */
for (i = 0; i < post_len; i++)
if (text_at(post_line, post_len, i, "PARITY:V4_POST ")) {
post_seen = 1;
post_clean = post_len >= sizeof clean - 1 && text_at(post_line, post_len, post_len - (unsigned)(sizeof clean - 1), clean);
break;
}
post_len = 0;
}
+139 -23
View File
@@ -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()
+163
View File
@@ -0,0 +1,163 @@
"""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"),
]