feat(v4.0.0): pictured output <# # #S HOLD SIGN #>; D-13

DECOMPOSITION.md 5.8 only named these as "standard pictured-output
definitions over UM/MOD and a hold buffer". They are now written out
(5.8) and run on the golden model.

D-13 (ruled 2026-10-03): the hold buffer takes 63 characters, as v3's;
HOLD of a value outside 0-255, or into a full buffer, stores nothing and
sets NODE-ERROR, which is what v3 did with its error flag.

Behaviour follows v3 otherwise: digits 0-9 then A-Z, BASE outside 2..36
reads as 10, SIGN ( n -- ). Stack effects are the standard ones, as 5.8
already ruled (v3 took its double low cell on top). The buffer is 64
bytes, filled backwards from its end through HLD; `#` divides by the
base in two UM/MOD steps. HOLD, SIGN and # end in a jump to the next
word instead of a call, which saves two return-stack entries: with
calls, the signed picture `.` needs overflowed the 9-deep return stack.

New test file v4/tests/test_pictured.c, on a node of its own:
test_foundation.c's hand-assembled words already fill 901 of a node's
1024 words. It re-assembles SWAP, OR, UM/MOD, LSHIFT, RSHIFT and C! as
they are in test_foundation.c.

Checked against a C reference (short division on 16-bit limbs) at 32-
and 64-bit cells, optimised and ASan+UBSan (`make test`, `make
sanitize`), 4716 checks per width: <# #S #>, <# # # 46 HOLD #S #> and
the signed picture, in bases 10, 16, 2, 8, 36, 3 and the invalid 0, 1,
37, -5, on every pair of 15 edge cells; HOLD of 12 values; and a base-2
number that overflows the buffer (63 characters kept, NODE-ERROR set,
nothing outside the buffer written). Six mutations all fail.

Headroom: <# #S #> leaves 3 data cells and 2 return entries; the signed
picture leaves 1 return entry. The depth is C!'s as written (C! ->
LSHIFT -> SWAP), which is unchanged.

Test results on the amd64 host only. This is a development check, not
acceptance (JUSTIFICATION.md section 16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-03 11:37:08 -04:00
co-authored by Claude Opus 5.5
parent 2e4d1330ea
commit 6a019b8968
2 changed files with 524 additions and 4 deletions
+28 -4
View File
@@ -147,8 +147,8 @@ IF body1 ELSE body2 THEN → if L1 drop body1 jump L2
times, as on the F18. `FOR ... UNEXT` is the same but the body must fit in one instruction word.
**Register conventions.** `A` and `B` are caller-saved. A word that uses them says so. Words in this
document that clobber `A`: `@ ! +! -! 2@ 2! C@ C! UM* * UM/MOD Q.FROM-INT Q.TO-INT Q.* Q./ Q.EXP Q.SQRT Q.LOG Q.SIN Q.COS SEND RECV`. Words that clobber `B`:
`Q./ Q.EXP Q.SQRT Q.LOG Q.SIN Q.COS SEND RECV`.
document that clobber `A`: `@ ! +! -! 2@ 2! C@ C! UM* * UM/MOD Q.FROM-INT Q.TO-INT Q.* Q./ Q.EXP Q.SQRT Q.LOG Q.SIN Q.COS HOLD SIGN # #S SEND RECV`. Words that clobber `B`:
`Q./ Q.EXP Q.SQRT Q.LOG Q.SIN Q.COS <# HOLD SIGN # #S #> SEND RECV`.
**Return-stack words** (`>R R> R@ 2>R 2R> 2R@ I J UNLOOP` and the loop runtimes) are always IN.
@@ -174,6 +174,7 @@ definition below depends on one, it says so.
| **D-10** | Q48.16 width on a 64-bit-cell node (ruled 2026-10-02). | **Two cells at every cell width.** A Q value is a signed double on 32- and 64-bit nodes alike, so every Q word is the same double word at both widths. At 32-bit cells this is bit-for-bit v3's 64-bit Q. At 64-bit cells the low cell is v3's value, and where v3 wraps on Q48.16 overflow (a sum past Q max, `ABS` or `NEG` of Q min) the high cell carries the true result instead. |
| **D-11** | `Q./` semantics: division by zero, rounding, overflow (ruled 2026-10-02). | **Saturate and flag; round toward zero; saturate on overflow.** The quotient of `a * 2^16 / b` is rounded toward zero, like `SM/REM`. When it does not fit a Q value it is clamped to Q max or Q min by its sign. Division by zero returns Q max or Q min by the sign of the dividend (`0 / 0` gives 0) and sets the node's `NODE-ERROR` register (§7), which `VM-ERROR?` reads. v3 returned 0 on division by zero and saturated whenever the dividend was 2^48 or more, even when the quotient would have fit. |
| **D-12** | Q approximations outside their domain (ruled 2026-10-02). | **Return 0 and set `NODE-ERROR`.** `Q.SQRT` of a negative value and `Q.LOG` of zero or a negative value return 0 and set `NODE-ERROR` (§7), as `Q./` does on division by zero (D-11). v3 returned 0 for `ln(0)` without a flag and read negative arguments as large unsigned values. |
| **D-13** | Pictured-output hold buffer: size and errors (ruled 2026-10-03). | **63 characters, as v3; on error set `NODE-ERROR` and drop the character.** The buffer holds 63 characters at either cell width. `HOLD` of a value outside 0–255, or into a full buffer, stores nothing and sets `NODE-ERROR` (§7), which is v3's behaviour (it set its error flag and dropped the character). A full double in base 2 therefore does not fit, as in v3. |
**Consequences of D-2 that every definition must respect.** The data stack holds 10 items and the
return stack 9, and every `call`, `FOR`, `DO` loop frame and `push` uses return-stack slots. Nesting
@@ -446,7 +447,8 @@ All output reaches the console through `EMIT` (DEV).
| Word | Fate | Notes |
| --- | --- | --- |
| `<#` `#` `#S` `HOLD` `SIGN` `#>` | CAP | Standard pictured-output definitions over `UM/MOD` and a hold buffer. v3's tolerant `#>` (pops `ud` only if present) is not kept; v4 follows the standard stack effect. |
| `<#` `#` `#S` `HOLD` `SIGN` `#>` | CAP | Pictured output over `UM/MOD` and a 64-byte hold buffer, filled backwards from its end `HEND` through the pointer `HLD`. Standard stack effects (v3 took its double low cell on top, and its tolerant `#>` popped `ud` only if present; neither is kept). Otherwise v3's behaviour: digits `0`–`9` then `A`–`Z`; `BASE` outside 2–36 reads as 10; 63 characters; `HOLD` of a value outside 0–255 or into a full buffer stores nothing and sets `NODE-ERROR` (D-13). Definitions below. Executed on the golden model (2026-10-03) against a C reference in bases 2, 3, 8, 10, 16, 36 and four invalid ones. `<# #S #>` leaves its caller 3 data cells and 2 return entries; the signed picture `dup push ABS 0 <# #S pop SIGN #>` leaves 1 return entry, the depth being `C!`'s as written (`C!` → `LSHIFT` → `SWAP`). `#`, `#S`, `HOLD` and `SIGN` clobber `A` and `B`. |
| `HLD` | CAP | Variable: byte address of the first held character. |
| `.` `.R` `U.` `U.R` `D.` `D.R` | CAP | Built on pictured output and `TYPE`. |
| `.S` | RET | No visible stack pointer (D-2). |
| `?` | CAP | `@ .` |
@@ -454,6 +456,28 @@ All output reaches the console through `EMIT` (DEV).
| `BASE` | CAP | Variable. |
| `DECIMAL` `HEX` `OCTAL` | CAP | `10 BASE !` and so on. |
```forth
\ pictured output. HEND is the byte address just past the hold buffer.
: (BASE) ( -- b ) BASE b! @b dup -2 + -if L1 drop drop 10 ;
L1: drop dup -37 + -if L2 drop ; L2: drop drop 10 ;
: <# ( -- ) HEND HLD b! !b ;
: HOLD ( c -- ) dup -256 and if OKC drop jump ERR
OKC: drop HLD b! @b -(HEND-62) + -if ROOM drop
ERR: drop NODE-ERROR b! -1 !b ;
ROOM: drop HLD b! @b -1 + dup !b jump C!
: SIGN ( n -- ) -if L1 drop 45 jump HOLD L1: drop ;
: # ( ud1 -- ud2 )
0 (BASE) UM/MOD push (BASE) UM/MOD pop ROT \ qlo qhi rem
dup -10 + -if L1 drop 48 + jump L2 L1: drop 55 + L2: jump HOLD
: #S ( ud -- 0 0 ) L: # over over OR if L1 drop jump L L1: drop ;
: #> ( ud -- baddr u ) drop drop HLD b! @b HEND over push inv pop + inv ;
```
`#` divides the double by the base in two `UM/MOD` steps, the high cell first and its remainder
leading the low cell; the last remainder is the digit. `HOLD`, `SIGN` and `#` end with a jump to the
next word rather than a call, so `C!` returns straight to their caller and two return-stack entries
are saved. `ROT` is in line.
### 5.9 Strings, parsing, and input
| Word | Fate | Notes |
@@ -781,4 +805,4 @@ Addresses are assigned in the node memory map (D-4). Names only here.
| `GOV-*` | R/W | Governor parameters and Jacquard selector state (`L8-*`, decay rate). |
| `REC-ENABLE` | R/W | DoE recorder on/off (`HB-ON` / `HB-OFF`). |
| `PORT-STATUS` | R | Per-port ready flags, for non-blocking polls. |
| `NODE-ERROR` | R/W | Arithmetic error flag: set to −1 by `Q./` on division by zero (D-11) and by `Q.SQRT` / `Q.LOG` outside their domain (D-12), cleared by writing 0. `VM-ERROR?` reads it. |
| `NODE-ERROR` | R/W | Arithmetic error flag: set to −1 by `Q./` on division by zero (D-11), by `Q.SQRT` / `Q.LOG` outside their domain (D-12) and by `HOLD` on a bad character or a full buffer (D-13), cleared by writing 0. `VM-ERROR?` reads it. |