From e06bb41c38637716524fef3a70a4fcc044105516 Mon Sep 17 00:00:00 2001 From: Simon Cooke Date: Sun, 9 Aug 2026 03:26:16 -0700 Subject: [PATCH 1/5] AI-Generated Documentation Pass Attempted to put together a starter-pack for the most confusing pieces of the SAM ROM, in more detail than the Annotated SAM BASIC pdf normally gives, particularly focusing on SAM BASIC, the FPC, and parameter passing using CALL/USR/USR$. This was generated by Claude Fable (thanks for the free tokens, Anthropic) reading the source code. As with everything AI, it may contain errors, but everything I've checked so far appears to be legit and match what I've seen digging through the assembly source recently. (And it's not like the technical manual/other docs don't have errors in them anyway...) - Cookie of Entropy (Simon Cooke) --- docs/constants.md | 674 +++++++++ docs/file-formats.md | 157 ++ docs/machine-code-interface.md | 303 ++++ docs/memory-map.md | 88 ++ docs/source-files.md | 2398 ++++++++++++++++++++++++++++++ docs/tokenized-program-format.md | 518 +++++++ 6 files changed, 4138 insertions(+) create mode 100644 docs/constants.md create mode 100644 docs/file-formats.md create mode 100644 docs/machine-code-interface.md create mode 100644 docs/memory-map.md create mode 100644 docs/source-files.md create mode 100644 docs/tokenized-program-format.md diff --git a/docs/constants.md b/docs/constants.md new file mode 100644 index 0000000..54aa03a --- /dev/null +++ b/docs/constants.md @@ -0,0 +1,674 @@ +# SAM Coupé ROM 3.0 — Constants Reference + +Every constant defined with `EQU` in the ROM source, grouped by the file that +defines it. Only six files define constants; all other source files consume +them. Values are given in hexadecimal (`&`-prefixed, as in the source) and +decimal where useful. + +| File | What it defines | +|---|---| +| [main.asm](#mainasm) | ROM page-select constants used with the paging ports | +| [vars.asm](#varsasm) | The entire system-variable map, buffer addresses, token codes, DOS hook codes, disc controller ports, save/load header offsets, and hardware I/O ports | +| [fpcmain.asm](#fpcmainasm) | The floating-point calculator (FPC) operation codes — the "instruction set" of the RST &28 calculator | +| [text.asm](#textasm) | Error-message compression codes (indexes into `COMPLIST`) | +| [miscx1.asm](#miscx1asm) | Lengths of ROM1 routine bodies that are copied into RAM buffers before execution | +| [miscx2.asm](#miscx2asm) | More copied-routine lengths (DEF KEYCODE, READ, DEF FN, tokenizer, MERGE) | + +--- + +## main.asm + +| Constant | Value | Description | +|---|---|---| +| `PAGE0` | 0 | RAM page 0 | +| `PAGE1` | 1 | RAM page 1 | +| `PAGE1F` | &1F | Page number 31 — written to port 250 (LRPORT) to select the system page in section B with ROM0 enabled | + +--- + +## vars.asm + +vars.asm contains no code: it is the memory map of the interpreter. All +"system variables" are `EQU` definitions. `VAR2` = **&5A00** is the base of the +main system-variable block (it must start on a page boundary). + +### Editor / device configuration block (initialised from `CHIT` table) + +| Constant | Value | Description | +|---|---|---| +| `VAR2` | &5A00 | Base of system variables (page-aligned) | +| `LNCUR` | VAR2+&00 | Current-line cursor character (usually `>`) | +| `KURCHAR` | VAR2+&01 | (2 bytes) Cursor characters — lower-case / upper-case | +| `BIN1DIG` | VAR2+&03 | Digit used by BIN$ for 1 (usually `1`) | +| `BIN0DIG` | VAR2+&04 | Digit used by BIN$ for 0 (usually `0`) | +| `INSTHASH` | VAR2+&05 | INSTR wildcard character, normally `#` | +| `PSLD` | VAR2+&06 | (2) Device letter/number (number = tape speed / disc number / net station) | +| `SPEEDINK` | VAR2+&08 | Ink flash counter reload value | +| `LINIPTR` | VAR2+&09 | (2) Pointer to line-interrupt palette-change table | +| `XCMDP` | VAR2+&0B | (3) Page/address of first external command list, or FFxxxx | +| `PRRHS` | VAR2+&0E | Printer right-hand-side limit (79) | +| `AFTERCR` | VAR2+&0F | &0A or NUL — whether auto line-feed needed after CR | +| `LPTPRT1` | VAR2+&10 | (2) Printer control port / strobe value (rest of block reserved for DUMP) | +| `TABVAR` | VAR2+&2F | 0 if TAB=16, else TAB=8 | +| `M23LSC` | VAR2+&30 | (2) Mode 2/3 lower-screen colours | +| `SOFE` | VAR2+&32 | Screen-off enable/disable flag (0=on) | +| `TPROMPTS` | VAR2+&33 | Bit 0: suppress printed names during LOAD; bit 1: suppress prompts during SAVE | + +### Screen/print variables (saved with a switched-out screen) + +| Constant | Value | Description | +|---|---|---| +| `BGFLG` | VAR2+&34 | Block graphics flag | +| `FL6OR8` | VAR2+&35 | 0 = 6-bit chars in MODE 2, NZ = 8-bit | +| `CSIZE` | VAR2+&36 | (2) Character height/width | +| `UWRHS` | VAR2+&38 | Upper window RHS (starts at 31) | +| `UWLHS` | VAR2+&39 | Upper window LHS (starts 0) | +| `UWTOP` | VAR2+&3A | Upper window top (starts 0) | +| `UWBOT` | VAR2+&3B | Upper window bottom (starts 18) | +| `LWRHS` | VAR2+&3C | Lower window RHS | +| `LWLHS` | VAR2+&3D | Lower window LHS | +| `LWTOP` | VAR2+&3E | Lower window top | +| `LWBOT` | VAR2+&3F | Lower window bottom (starts 20) | +| `MODE` | VAR2+&40 | Current screen mode 0–3 | +| `YCOORD` | VAR2+&41 | Graphics Y position, 0–191, 0 at top | +| `XCOORD` | VAR2+&42 | (2) Graphics X position 0–255 (fat) or 0–511 (thin) | +| `RLINE` | = XCOORD | Alias used by ROLL | +| `THFATP` | VAR2+&44 | 0 = thin pixels, NZ = fat (permanent) | +| `ATTRP` | VAR2+&45 | Attribute used by modes 0 and 1 (permanent) | +| `MASKP` | VAR2+&46 | Attribute mask (permanent) | +| `PFLAGP` | VAR2+&47 | Bit 4 = PAPER 9, bit 6 = INK 9 (permanent) | +| `M23PAPP` | VAR2+&48 | Mode 2/3 paper byte (permanent) | +| `M23INKP` | VAR2+&49 | Mode 2/3 ink byte (permanent, must precede OVERP) | +| `OVERP` | VAR2+&4A | OVER state 0–1 (permanent) | +| `INVERP` | VAR2+&4B | 00/FF = normal/inverse (permanent) | +| `GOVERP` | VAR2+&4C | Graphics over mode 0–3: normal, XOR, OR, AND (used by PUT) | +| `THFATT` | VAR2+&4D | Temporary copy of THFATP (forced fat outside MODE 2) | +| `ATTRT` | VAR2+&4E | Temporary attribute | +| `MASKT` | VAR2+&4F | Temporary mask | +| `PFLAGT` | VAR2+&50 | Temporary print flags | +| `M23PAPT` | VAR2+&51 | Temporary mode 2/3 paper | +| `M23INKT` | VAR2+&52 | Temporary mode 2/3 ink | +| `OVERT` | VAR2+&53 | Temporary OVER | +| `INVERT` | VAR2+&54 | Temporary inverse | +| `GOVERT` | VAR2+&55 | Temporary graphics-over | +| `WINDRHS` | VAR2+&56 | Current window RHS (temp) | +| `WINDLHS` | VAR2+&57 | Current window LHS | +| `WINDTOP` | VAR2+&58 | Current window top | +| `WINDBOT` | VAR2+&59 | Current window bottom | +| `WINDMAX` | VAR2+&5A | (2) Upper window lowest bottom / max RHS | +| `ORGOFF` | VAR2+&5C | Graphics origin offset | +| `LSOFF` | VAR2+&5D | Lower screen bit offset | +| `SPOSNU` | VAR2+&6C | (2) Upper screen print position | +| `SPOSNL` | VAR2+&6E | (2) Lower screen print position | + +### Interpreter state + +| Constant | Value | Description | +|---|---|---| +| `PRPOSN` | VAR2+&70 | (2) Printer position | +| `OPCHAR` | VAR2+&72 | Current output char (LPRINT) | +| `DEVICE` | VAR2+&73 | 0=upper screen, 1=lower screen, 2=printer… | +| `CLET` | VAR2+&74 | Current channel letter (K/S/P/B/T/$ etc.) | +| `IFTYPE` | VAR2+&75 | Long/short IF flag | +| `REFFLG` | VAR2+&76 | Z if a REF variable being worked on (also: NZ = FN used in line, for compiler) | +| `CURDISP` | VAR2+&77 | Current display number | +| `CUSCRNP` | VAR2+&78 | Current screen page (includes mode bits) | +| `CURP` | VAR2+&79 | Current upper RAM port value | +| `CLRP` | VAR2+&7A | Current lower RAM port (temp store during paging) | +| `CSA` | VAR2+&7B | (2) Current statement address | +| `FIRST` | VAR2+&7D | (2) First line number for LIST x TO y (also array slicer) | +| `LAST` | VAR2+&7F | (2) Last line number for LIST x TO y (also array slicer) | + +### Memory-area pointers (page byte + 16-bit address; adjusted by MAKEROOM/RECLAIM) + +Each is a page number (`…P`) immediately followed by a 16-bit address in the +&8000–&BFFF range. They must stay in this order: + +| Page var | Addr var | Value | Points to | +|---|---|---|---| +| `SAVARSP` | `SAVARS` | VAR2+&81/&82 | String/array variables area | +| `NUMENDP` | `NUMEND` | VAR2+&84/&85 | End of numeric variables | +| `NVARSP` | `NVARS` | VAR2+&87/&88 | Numeric variables area (= end of program) | +| `DATADDP` | `DATADD` | VAR2+&8A/&8B | DATA list read pointer | +| `WKENDP` | `WKEND` | VAR2+&8D/&8E | End of work space | +| `WORKSPP` | `WORKSP` | VAR2+&90/&91 | Work space | +| `ELINEP` | `ELINE` | VAR2+&93/&94 | Edit line | +| `CHADP` | `CHAD` | VAR2+&96/&97 | Interpretation pointer (current character) | +| `KCURP` | `KCUR` | VAR2+&99/&9A | Editor cursor position | +| `NXTLINEP` | `NXTLINE` | VAR2+&9C/&9D | Address of next program line | +| `PROGP` | `PROG` | VAR2+&9F/&A0 | Start of BASIC program | +| `XPTRP` | `XPTR` | VAR2+&A2/&A3 | Syntax-error position marker (`?`) | +| `DESTP` | `DEST` | VAR2+&A5/&A6 | Assignment destination | +| `PRPTRP` | `PRPTR` | VAR2+&A8/&A9 | PROC pointer (call-site parameter list) | +| `DPPTRP` | `DPPTR` | VAR2+&AB/&AC | DEF PROC pointer (not adjusted) | +| `CLAPG` | `CLA` | VAR2+&AE/&AF | Current line address (not adjusted) | + +### Miscellaneous flags and stores + +| Constant | Value | Description | +|---|---|---| +| `DFTFB` | VAR2+&B1 | DEFAULT flag byte | +| `STRNO` | VAR2+&B2 | Current stream number | +| `LDCO` | VAR2+&B3 | LOAD ZX code offset (pages) | +| `OPSTORE` | VAR2+&B5 | (2) Output-address store (control-code handling) | +| `DMPFG` | VAR2+&B7 | NZ = print output dumped (discarded) | +| `LISTFLG` | VAR2+&B8 | 0/1/2 = LIST FORMAT 0/1/2 (pretty-listing indent) | +| `LSTFT` | VAR2+&B9 | Temporary LISTFLG used by channel "R" | +| `INQUFG` | VAR2+&BA | In-quotes flag: bit 0 = 1 inside quotes (tokens not expanded) | +| `SPROMPT` | VAR2+&BB | NZ = no "scroll?" prompts | +| `OLDSPCS` | VAR2+&BC | Space status of previous listed line | +| `INDOPFG` | VAR2+&BD | Indented output flag | +| `NXTSPCS` | VAR2+&BE | Next-line leading spaces (list indenting) | +| `CURSPCS` | VAR2+&BF | Current-line leading spaces | +| `NXTHSPCS` | VAR2+&C0 | Next-line half spaces | +| `CURTHSPCS` | VAR2+&C1 | Current-line half spaces | +| `KPOS` | VAR2+&C2 | (2) Cursor screen position | +| `SOFFCT` | VAR2+&C4 | Screen-off counter (next 4 must stay in order) | +| `SOFLG` | VAR2+&C5 | NZ = screen has been turned off | +| `SPEEDIC` | VAR2+&C6 | Flashing-ink counter | +| `PALFLAG` | VAR2+&C7 | Bit 0 shows which palette table is in use | +| `TEMPW1`–`TEMPW3` | VAR2+&C8/&CA/&CC | (2 each) Temporary words | +| `TEMPB1`–`TEMPB3` | VAR2+&CE/&CF/&D0 | Temporary bytes (TEMPB3 = final FILL param / CALL param count) | +| `LASTSTAT` | VAR2+&D1 | STATPORT value on last interrupt | +| `SPSTORE` | VAR2+&D2 | (2) SP store, exclusive to interrupts | +| `JVSP` | VAR2+&D5 | (2) JSVIN SP store | +| `NMISP` | VAR2+&D7 | (2) NMI SP store | +| `NMILRP` | VAR2+&D9 | LRPORT value when NMI occurred | + +### Vector table (all 2 bytes each) + +`VECTBS` (= VAR2+&DA) is the base. Setting a vector non-zero diverts the +corresponding ROM routine. + +| Vector | Value | Hooked function | +|---|---|---| +| `DMPV` | VAR2+&DA | DUMP command | +| `SETIYV` | VAR2+&DC | Interrupt IY setup | +| `PRTOKV` | VAR2+&DE | Print-token | +| `NMIV` | VAR2+&E0 | NMI (normally super-break) | +| `FRAMIV` | VAR2+&E2 | Frame interrupt | +| `LINIV` | VAR2+&E4 | Line interrupt | +| `COMSV` | VAR2+&E6 | Communications interrupt | +| `MIPV` | VAR2+&E8 | Main input | +| `MOPV` | VAR2+&EA | Main output | +| `EDITV` | VAR2+&EC | Editor entry | +| `RST8V` | VAR2+&EE | RST &08 (error) | +| `RST28V` | VAR2+&F0 | RST &28 (FP calculator) | +| `RST30V` | VAR2+&F2 | RST &30 called from outside ROM0 | +| `CMDV` | VAR2+&F4 | Command dispatch (add new commands) | +| `EVALUV` | VAR2+&F6 | Function evaluation (add new functions) | +| `LPRTV` | VAR2+&F8 | LPRINT output | +| `MTOKV` | VAR2+&FA | Tokenizer extension (extra keyword matching) | +| `MOUSV` | VAR2+&FC | Mouse read | +| `KURV` | VAR2+&FE | Cursor | + +### Print expansion tables and compiler/error state + +| Constant | Value | Description | +|---|---|---| +| `CEXTAB` | VAR2+&0100 | (32) Colour-applied expansion table (mode 2/3 printing) | +| `EXTAB` | VAR2+&0120 | (32) 16 words of mode-3 nibble→word expansion (or 16 bytes of mode-2 doubled data) | +| `COMPFLG` | VAR2+&0140 | Flag bits used by label/FN/PROC compiler (bit 7 = whole program needs compiling) | +| `BREAKDI` | VAR2+&0141 | NZ = BREAK between statements disabled | +| `ERRSTAT` | VAR2+&0142 | Statement number for ON ERROR | +| `ERRLN` | VAR2+&0143 | (2) Line to GOTO on error | +| `ONERRFLG` | VAR2+&0145 | Bit 7 = temporary ON ERROR, bit 0 = permanent | +| `ONSTORE` | VAR2+&0146 | ON command's statement number | +| `BCSTORE` | VAR2+&0147 | (2) BC store used by RST &30 | +| `M3PAPP` | VAR2+&0149 | (2) Mode 3 paper (saved across mode 2) | +| `M3LSC` | VAR2+&014B | (2) Mode 3 lower-screen colours | +| `TEMPW4`/`TEMPW5` | VAR2+&014D/&014F | (2 each) Used by POINTERS | +| `LPT` | VAR2+&0152 | (30) Line-pointer table: one byte per screen line, FF if a line number starts there | +| `ANYIV` | VAR2+&0170 | (2) "Any interrupt" vector | +| `RNSTKE` | VAR2+&0172 | (2) Rename stack pointer (parameter processing) | +| `CURCMD` | VAR2+&0174 | Token code of command being executed | +| `LTDFF` | VAR2+&0175 | LET/DEFAULT flag | +| `STRM16NM` | VAR2+&0176 | (11) Type/length byte + name of the variable that stream 16 writes to | +| `GRARF` | VAR2+&0181 | Graphics record flag (0=off) | +| `DHADJ` | VAR2+&0182 | Double-height adjust | +| `PAGCOUNT` | VAR2+&0183 | Page counter used by FARLDIR | +| `MODCOUNT` | VAR2+&0184 | (2) Mod-16K counter used by FARLDIR | +| `BCREG` | VAR2+&0186 | (2) The FP calculator's B register (BREG) | +| `AUTOFLG` | VAR2+&0188 | AUTO mode on/off | +| `AUTOSTEP` | VAR2+&0189 | (2) AUTO step (alias `RSTEP` = RENUM step) | +| `LSPTR` | VAR2+&018B | (2) Line scan pointer | +| `LNPTR` | VAR2+&018D | Screen line holding the `>` cursor (>&3F = none) | +| `MSEDP` | VAR2+&018E | (8) Mouse driver data | +| `BUTSTAT` | VAR2+&018F | Mouse button status | +| `MXCRD` | VAR2+&0196 | (2) Mouse X coordinate | +| `MYCRD` | VAR2+&0198 | (2) Mouse Y coordinate | + +### PRINTFP (number formatting) workspace + +| Constant | Value | Description | +|---|---|---| +| `FRACLIM` | VAR2+&019A | Fraction digit limit | +| `NPRPOS` | VAR2+&019B | (2) Number print position | +| `DIGITS` | VAR2+&019D | Digit count (these four must stay in order) | +| `EPOWER` | VAR2+&019E | Exponent power | +| `DECPNTED` | VAR2+&019F | Decimal point emitted flag | +| `PRNBUFF` | VAR2+&01A0 | (16) Number print buffer | +| `BCDBUFF` | VAR2+&01B0 | (5) BCD conversion buffer | + +### Save/load & DOS state + +| Constant | Value | Description | +|---|---|---| +| `OTHER` | VAR2+&01B5 | Net destination station number | +| `DCT` | VAR2+&01B6 | Disc error counter | +| `SLDEV` | VAR2+&01B7 | (2) Temporary device letter/number | +| `OVERF` | VAR2+&01B9 | SAVE OVER flag (0 = save over allowed) | +| `INSLV` | VAR2+&01BA | (2) (Unused insert-line vector) | +| `STRLOCN` | VAR2+&01BC | (2) Used by LOOKVARS — location of found string | +| `TVDATA` | VAR2+&01BE | (2) Control-code parameter store | +| `DOSER` | VAR2+&01C0 | (2) Jump vector after DOS execution | +| `DOSFLG` | VAR2+&01C2 | Z if no DOS loaded, else DOS page | +| `DOSCNT` | VAR2+&01C3 | Bit 0 set if DOS in control | +| `BSTKEND` | VAR2+&01C4 | (2) BASIC (DO/GOSUB/PROC) stack end | + +### Init block (26 bytes initialised from `MAIT` table) + +| Constant | Value | Description | +|---|---|---| +| `BASSTK` | VAR2+&01C6 | (2) BASIC stack base | +| `HEAPEND` | VAR2+&01C8 | (2) Heap end | +| `HPST` | VAR2+&01CA | (2) Heap start | +| `FPSBOT` | VAR2+&01CC | (2) Bottom of FP calculator stack | +| `DKDEF` | VAR2+&01CE | (2) DEF KEY definitions start | +| `DKLIM` | VAR2+&01D0 | (2) DEF KEY buffer growth limit | +| `PATOUT` | VAR2+&01D2 | (2) Address of "printable chars" output routine | +| `ERRMSGS` | VAR2+&01D4 | (2) Error message table address | +| `UMSGS` | VAR2+&01D6 | (2) Utility message table address | +| `KBTAB` | VAR2+&01D8 | (2) Keyboard table address | +| `CMDADDRT` | VAR2+&01DA | (2) Command address table (in ROM1) | +| `MNOP` | VAR2+&01DC | (2) Main output routine address | +| `MNIP` | VAR2+&01DE | (2) Main input routine address | +| `PAGER` | VAR2+&01E0 | (14) Reserved for paging subroutine | +| `KBUFF` | VAR2+&01EE | (18) Two 72-bit keyboard state tables | + +### ZX-compatible system variables (&5C00 block) + +| Constant | Value | Description | +|---|---|---| +| `LHM1` | &5C00 | Used by KEYSCAN as LASTH-1 (LASTH = &5C01 = last key hit) | +| `KDATA` | &5C02 | Control-code store during colour-parameter input | +| `LKPB` | &5C03 | (2) Last keyboard state | +| `REPCT` | &5C05 | Repeat counter | +| `LASTKV` | &5C06 | (2) Last key vector | +| `LASTK` | &5C08 | Key from buffer queue head | +| `REPDEL` | &5C09 | Repeat delay | +| `REPPER` | &5C0A | Repeat period | +| `STREAMS` | &5C10 | (42) Stream displacements for streams −5 to 15 (16 maps to −4; table physically at &5C0C–&5C35) | +| `CHARS` | &5C36 | (2) Character set pointer (address − 256) | +| `RASP` | &5C38 | Warning buzz length | +| `PIP` | &5C39 | Key-click length | +| `ERRNR` | &5C3A | Error number (0 = OK) | +| `FLAGS` | &5C3B | Main flags: bit 7 = running (vs syntax check), bit 6 = numeric result, bit 5 = new key, bit 0 = leading space suppressed | +| `TVFLAG` | &5C3C | TV flags: bit 5 = clear lower screen on keystroke, bit 4 = autolist, bit 3 = print edit line to lower screen | +| `ERRSP` | &5C3D | (2) Error stack pointer | +| `LISTSP` | &5C3F | (2) SP for aborting AUTOLIST at screen end | +| `NEWPPC` | &5C42 | (2) Line number to jump to | +| `NSPPC` | &5C44 | Statement to jump to (&FF = no jump pending) | +| `PPC` | &5C45 | (2) Current line number | +| `SUBPPC` | &5C47 | Current statement number within line | +| `BORDCR` | &5C48 | Lower-screen attributes in modes 1/2 | +| `EPPC` | &5C49 | (2) Current edit line number (cursor `>` line) | +| `BORDCOL` | &5C4B | Border port value | +| `CHANS` | &5C4F | (2) Channels area pointer | +| `CURCHL` | &5C51 | (2) Current channel pointer | +| `DEFADDP`/`DEFADD` | &5C53/&5C54 | Page/address of DEF FN parameter bracket during FN evaluation (0 = none) | +| `NLASTH` | &5C56 | (3) New last-key data | +| `ZIPLIB` | &5C61 | (2) Reserved for Simon N. Goodwin's ZIP compiler | +| `ZIPTEMP` | &5C63 | (2) ZIP compiler temp | +| `STKEND` | &5C65 | (2) End of FP calculator stack (first free byte) | +| `KPFLG` | &5C67 | Keypad flag: even = function keys, odd = number pad | +| `MEM` | &5C68 | (2) Calculator memory-area pointer | +| `FLAGS2` | &5C6A | Bit 3 = caps lock, bit 0 = screen not clear | +| `SDTOP` | &5C6C | (2) Line number at top of AUTOLIST screen | +| `OLDPPC` | &5C6E | (2) Line for CONTINUE | +| `OSPPC` | &5C70 | Statement for CONTINUE | +| `FLAGX` | &5C71 | Bit 5 = INPUT mode, bit 0 = variable doesn't exist yet | +| `STRLEN` | &5C72 | (2) String length in assignments | +| `SEED` | &5C76 | (2) RND seed | +| `FRAMES` | &5C78 | (3) Frame counter | +| `UDG` | &5C7B | (2) User-defined graphics pointer | +| `HUDG` | &5C7D | (2) High UDG pointer | +| `FRAMES34` | &5C7F | (2) Frames bytes 3–4 (5-byte frame counter) | +| `OLDPOS` | &5C82 | Previous print position | +| `SCRCT` | &5C8C | Scroll count (lines before "scroll?" prompt) | +| `KBQB` | &5C8D | (8) Keyboard queue | +| `KBQP` | &5C95 | (2) Keyboard queue pointers (low = end, high = head) | +| `SCPTR` | &5C9D | (2) Address of current screen in SCLIST | +| `FISCRNP` | &5C9F | Page of screen 1 (not cleared by NEW, from here on) | +| `SCLIST` | &5CA0 | (16) Screens list: mode/page of screens 1–16, or &FF | +| `LASTPAGE` | &5CB0 | Last page reserved by BASIC | +| `RAMTOPP`/`RAMTOP` | &5CB1/&5CB2 | Page/address of RAMTOP | +| `PRAMTP` | &5CB4 | Last physical page present in machine | + +### Token codes and keyword count + +| Constant | Value | Description | +|---|---|---| +| `KEYWNO` | &C4 (196) | Number of words in the keyword table | +| `TSPEED` | 112 | Default tape speed | +| `PITOK` | &3B | PI function token (first function code) | +| `PI` | &21 (PITOK−&1A) | PI's internal evaluator code (tokens are stored −&1A internally) | +| `INSTOK` | &4A | INSTR token — separates numeric from string immediate functions | +| `INSTR` | &30 (INSTOK−&1A) | INSTR internal code | +| `FNTOK` | &42 | FN function token | +| `BINTOK` | &43 | BIN function token (also recognised by CALC5BY for binary literals) | +| `SCRNTOK` | &4C | SCREEN$ token (used by SAVE/LOAD) | +| `SINTOK` | &53 | SIN token (first FPC-handled function) | +| `INTOK` | &60 | IN token | +| `CODETOK` | &6C | CODE token (used by SAVE/LOAD) | +| `CHRSTOK` | &70 | CHR$ token (used by COPY) | +| `MODTOK` | &7A | MOD token (first alphabetic binary operator) | +| `ANDTOK` | &80 | AND token | +| `USINGTOK` | &85 | USING qualifier token (first single-byte token) | +| `ATTOK` | &87 | AT | +| `TABTOK` | &88 | TAB | +| `WHILETOK` | &8A | WHILE | +| `UNTILTOK` | &8B | UNTIL | +| `LINETOK` | &8C | LINE | +| `THENTOK` | &8D | THEN | +| `TOTOK` | &8E | TO | +| `STEPTOK` | &8F | STEP | +| `SAVETOK` | &94 | SAVE command token | +| `LOADTOK` | &95 | LOAD | +| `MERGETOK` | &96 | MERGE | +| `VERIFYTOK` | &97 | VERIFY | + +(Commented out in source: `FORMATTOK`=&91, `ERASETOK`=&92, `RECORDTOK`=&EF.) + +### DOS hook codes (invoked via `RST &08` with code ≥ 128) + +| Constant | Value | Description | +|---|---|---| +| `BTHK` | 128 | DOS boot (DOS can ignore, or treat as ALHK) | +| `FOPHK` | 129 | DOS open (get header) | +| `LDHK` | 130 | DOS load | +| `VFYHK` | 131 | DOS verify | +| `SVHK` | 132 | DOS save | +| `OSHK` | 134 | DOS open stream | +| `CSHK` | 135 | DOS close stream | +| `ALHK` | 136 | DOS load auto-load file (follows BOOT) | +| `DIRHK` | 137 | DOS directory | +| `DVHK` | 139 | DOS DVAR | +| `EOFHK` | 140 | DOS EOF | +| `PTRHK` | 141 | DOS PTR | +| `PATHHK` | 142 | DOS PATH$ | + +### Disc controller (WD1772) ports and commands + +| Constant | Value | Description | +|---|---|---| +| `COMM` | 224 | Command/status port | +| `TRCK` | 225 | Track port | +| `SECT` | 226 | Sector port | +| `DTRQ` | 227 | Data port | +| `DRES` | 9 | Restore command | +| `STPIN` | &59 | Step-in command | +| `STPOUT` | &79 | Step-out command | +| `DRSEC` | &80 | Read-sector command | + +### Save/load header offsets + +| Constant | Value | Description | +|---|---|---| +| `HFG` | 15 | Displacement to header flag | +| `HDT` | 26 | Displacement to header date/time | +| `HDN` | 31 | Displacement to header numbers | +| `HDRL` | 80 (&50) | Header buffer length | +| `NMLEN` | 10 | Max file name length | +| `YOSDISP` | 57 | Displacement of YOS (Y offset) system pseudo-variable | +| `YRGDISP` | 67 | Displacement of YRG (Y range) | +| `XOSDISP` | 77 | Displacement of XOS | +| `XRGDISP` | 87 | Displacement of XRG | +| `RSBUFF` | &E003 | ROM-load save buffer | +| `SBO` | &8000 | Section B origin | +| `SBN` | &C000 | Section C origin | + +### Fixed buffer addresses (system page, section A when paged at &4000) + +| Constant | Value | Description | +|---|---|---| +| `HPEND` | &4000 | Heap end | +| `BSTACK` | &4AFF | BASIC (DO/LOOP/PROC/GOSUB) stack base (grows down) | +| `HDR` | &4B00 | Save header buffer (&50 bytes; also PARPRO rename stack) | +| `HDL` | &4B50 | Loaded header buffer | +| `INTSTK` | &4C00 | Interrupt stack (uses down to ~&49EE) | +| `BUFF256` | &4C00 | 256-byte buffer (page aligned) | +| `FPSB` | &4D00 | Bottom of FP calculator stack | +| `CDBUFF` | &4D00 | Code buffer for generated multi-LDI/RLD code (max &181 bytes; the RAM tokenizer also runs at CDBUFF+&80) | +| `ISPVAL` | &4F00 | Initial SP value | +| `INSTBUF` | &4F00 | Buffer for code copied from ROM1, etc. (&200 bytes) | +| `MSGBUFF` | INSTBUF+&1C0 | Message assembly buffer | +| `FILBUFF` | &5080 | File buffer | +| `ALLOCT` | &5100 | Page allocation table (32 bytes, 1 per page, + terminator; page aligned) | +| `MEMVAL` | &5121 | Memory test value | +| `TLBYTE` | &513F | Type/length byte of variable name being looked up | +| `NMBUFF` | &5140 | Variable/FN/PROC name buffer (alias `FIRLET` = first letter) | +| `NMISTK` | &5188 | NMI stack | +| `SCRNBUF` | &5188 | 8 bytes used by SCREEN$ for compressed form | +| `CHARSVAL` | &5190 | Expanded character set (copied from compressed CHARSRC) | +| `PALTAB` | &55D8 | Working palette table | +| `LINICOLS` | &5600 | Line-interrupt colour table | +| `DKBU` | &5800 | DEF KEY buffer | +| `KTAB` | &58E0 | Keyboard map | +| `PVBUFF` | &FEB0 | Print vars of the non-displayed screen (in second screen page) | +| `FILLSTK` | = PVBUFF | FILL command stack | +| `PALBUF` | &FFD8 | Palette of non-displayed screen | + +### Hardware I/O ports + +| Constant | Value | Description | +|---|---|---| +| `SNDPORT` | &FF | Sound chip (address reg at &1FF, data at &FF) | +| `KEYPORT` | &FE | Keyboard | +| `MDIPORT` | &FD | MIDI | +| `VIDPORT` | &FC | Video mode/page | +| `URPORT` | &FB | Upper RAM page select (section C/D) | +| `LRPORT` | &FA | Lower RAM page select (section A/B) + ROM0/ROM1 enable bits | +| `STATPORT` | &F9 | Status/line-interrupt | +| `CLUTPORT` | &F8 | Palette (colour look-up table) | + +--- + +## fpcmain.asm + +These are the operation codes of the floating-point calculator — the byte +codes that follow `RST &28` (`DB CALC` … `DB EXIT`) in calculator literals. +See [tokenized-program-format.md](tokenized-program-format.md) for how the +calculator is driven. + +### Binary operations (&00–&1F) + +| Constant | Value | Operation | +|---|---|---| +| `MULT` | &00 | Multiply | +| `ADDN` | &01 | Add (numeric) | +| `CONCAT` | &02 | String concatenation | +| `SUBN` | &03 | Subtract | +| `POWER` | &04 | Raise to power | +| `DIVN` | &05 | Divide | +| `SWOP` | &06 | Swap top two stack entries | +| `DROP` | &07 | Drop top entry | +| `MOD` | &08 | Modulo | +| `IDIV` | &09 | Integer divide (DIV) | +| `NUOR` | &0D | Numeric OR | +| `NUAND` | &0E | Numeric AND | +| `NNOTE` | &0F | Numeric <> | +| `NLESE` | &10 | Numeric <= | +| `NGRTE` | &11 | Numeric >= | +| `NLESS` | &12 | Numeric < | +| `NEQUAL` | &13 | Numeric = | +| `NGRTR` | &14 | Numeric > | +| `SAND` | &15 | String AND (string vs number) | +| `SNOTE` | &16 | String <> | +| `SLESE` | &17 | String <= | +| `SGRTE` | &18 | String >= | +| `SLESS` | &19 | String < | +| `SEQUAL` | &1A | String = | +| `SGRTR` | &1B | String > | +| `SWOP13` | &1C | Swap 1st and 3rd stack entries | +| `SWOP23` | &1D | Swap 2nd and 3rd stack entries | +| `JPTRUE` | &1E | Jump (following displacement byte) if true | +| `JPFALSE` | &1F | Jump if false | + +(Codes &0A–&0C are the gap between IDIV and NUOR: unused.) + +### Control / stack manipulation (&20–&34) + +| Constant | Value | Operation | +|---|---|---| +| `JUMP` | &20 | Unconditional jump (displacement byte follows) | +| `LDBREG` | &21 | Load BREG from following byte | +| `DECB` | &22 | Decrement BREG, jump if not zero (displacement follows) | +| `STKBREG` | &23 | Stack BREG as a number | +| `USEB` | &24 | Execute operation code held in BREG | +| `DUP` | &25 | Duplicate top entry | +| `ONELIT` | &26 | Stack 1-byte literal (sign-extended small integer) | +| `FIVELIT` | &27 | Stack 5-byte literal (follows inline) | +| `SOMELIT` | &28 | Stack several literals (count byte follows) | +| `LKADDRB` | &29 | Look up address, byte displacement | +| `LKADDRW` | &2A | Look up address, word displacement | +| `REDARG` | &2B | Reduce argument (trig range reduction) | +| `LESS0` | &2C | True if < 0 | +| `LESE0` | &2D | True if <= 0 | +| `GRTR0` | &2E | True if > 0 | +| `GRTE0` | &2F | True if >= 0 | +| `TRUNC` | &30 | Truncate to integer | +| `RESTACK` | &31 | Re-stack (normalise to 5-byte FP form) | +| `POWR2` | &32 | Power of 2 | +| `EXIT` | &33 | Leave calculator | +| `EXIT2` | &34 | Leave calculator (alternate) | + +### Functions (&39–&5D) — dispatched from expression evaluator codes + +| Constant | Value | Function | +|---|---|---| +| `SIN` | &39 | Sine | +| `COS` | &3A | Cosine | +| `TAN` | &3B | Tangent | +| `ASN` | &3C | Arcsine | +| `ACS` | &3D | Arccosine | +| `ATN` | &3E | Arctangent | +| `LOGN` | &3F | Natural log (LN) | +| `EXP` | &40 | Exponential | +| `ABS` | &41 | Absolute value | +| `SGN` | &42 | Sign | +| `SQR` | &43 | Square root | +| `INT` | &44 | Integer (floor) | +| `INP` | &46 | IN (port read) | +| `PEEK` | &47 | PEEK | +| `EOF` | &4C | EOF | +| `UDGA` | &4F | UDG address | +| `LEN` | &51 | String length | +| `CODE` | &52 | First-character code | +| `VALS` | &53 | VAL$ | +| `VAL` | &54 | VAL | +| `CHRS` | &56 | CHR$ | +| `STRS` | &57 | STR$ | +| `INKEY` | &5B | INKEY$ | +| `NOT` | &5C | Logical NOT | +| `NEGATE` | &5D | Unary minus | + +### Calculator-literal prefix codes (&C8–&FF ranges) + +| Constant | Value | Operation | +|---|---|---| +| `CALC` | &EF | The `RST &28` opcode value — `DB CALC` starts an inline calculator program | +| `STOD0`–`STOD4` | &C8–&CC | Store top of stack to memory slot 0–4 and drop | +| `STO0`–`STO5` | &D0–&D5 | Store top of stack to memory slot 0–5 (keep) | +| `RCL0`–`RCL5` | &D8–&DD | Recall memory slot 0–5 onto the stack | +| `STKHALF` | &E0 | Stack constant 0.5 | +| `STKZERO` | &E1 | Stack constant 0 | +| `STK16K` | &E2 | Stack constant 16384 | +| `STKFONE` | &E6 | Stack constant 1 in full FP form | +| `STKONE` | &E9 | Stack constant 1 | +| `STKTEN` | &EC | Stack constant 10 | +| `STKHALFPI` | &F0 | Stack constant π/2 | + +--- + +## text.asm + +Error messages are compressed: codes 0–31 in a message expand to a common +word/fragment from `COMPLIST`. These constants are the fragment indexes +(each fragment shown with its exact text, including significant leading / +trailing spaces). + +| Constant | Value | Expansion text | +|---|---|---| +| `INVALID` | 0 | `Invalid ` | +| `WITHOUT` | 1 | ` without ` | +| `MISSING` | 2 | `Missing ` | +| `TOO` | 3 | ` too ` | +| `ISALREDOP` | 4 | ` is already open` | +| `PALET` | 5 | `palette ` | +| `SOFS` | 6 | ` of ` | +| `ERROR` | 7 | ` error` | +| `TREAM` | 8 | `tream` | +| `UMBER` | 9 | `umber` | +| `CNXT` | 10 | `NEXT` | +| `NO` | 11 | `No ` | +| `BREAK` | 12 | `BREAK ` | +| `CREEN` | 13 | `creen` | +| `ARRAY` | 14 | ` array: ` | +| `NOTE` | 15 | `Note` | +| `LONG` | 16 | `long` | +| `SNOTS` | 17 | ` not ` | +| `SNAME` | 18 | ` name` | +| `STRING` | 19 | `String` | +| `TOOMANY` | 20 | `Too many ` | +| `TATEMENT` | 21 | `tatement` | +| `CSTOP` | 22 | `STOP ` | +| `FILE` | 23 | `file` | +| `CLOUR` | 24 | `colour` | +| `CIN` | 25 | `in` | +| `CPROC` | 26 | ` PROC` | +| `CLOOP` | 27 | `LOOP` | +| `CTO` | 28 | `to` | +| `CDE` | 29 | `de` | +| `CME` | 30 | `me` | +| `CAR` | 31 | `ar` | + +--- + +## miscx1.asm + +ROM1 cannot be executed while the BASIC program area is paged into section C, +so several command bodies are copied from ROM1 into RAM buffers +(`INSTBUF`/`CDBUFF`) and executed there. These constants are the *lengths* of +those relocatable bodies, computed from their start/end labels; the stubs in +misc2.asm sum them to find each body's source address in ROM1 (they are +assembled consecutively from &C000). + +| Constant | Value expression | Body | +|---|---|---| +| `RENLN` | TRANSF+3−RNMP2 | RENUM part 2 | +| `GETLN` | GT4+6−GETP2 | GET | +| `DELLN` | DELFIN+3−DELPT2 | DELETE | +| `KEYLN` | KEYFIN+3−KEYP2 | KEYIN | +| `POPLN` | POP5−POPP2+1 | POP | +| `INPLN` | INPFIN+1−INPP2 | INPUT | + +## miscx2.asm + +| Constant | Value expression | Body | +|---|---|---| +| `DKLN` | DKFIN+1−DKP2 | DEF KEYCODE | +| `RDLN` | 0 | READ (now executes in place; length retained as 0) | +| `DFNLN` | DFNNS+2−DFNP2 | DEF FN | +| `TOKLN` | TOKFIN+1−TOKPT2 | The tokenizer (runs at CDBUFF+&80) | +| `MELN` | MEEND+1−MEPRO2 | MERGE | + +## mult.asm + +Defines no constants (a comment mentions `EQU` only in describing DOUBLE +FPFORM's behaviour). + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. diff --git a/docs/file-formats.md b/docs/file-formats.md new file mode 100644 index 0000000..9f38871 --- /dev/null +++ b/docs/file-formats.md @@ -0,0 +1,157 @@ +# SAM Coupé ROM 3.0 — Saved BASIC Program File Format + +How `SAVE`/`LOAD` encode a BASIC program on tape, net or disc, from +[tapemn.asm](../tapemn.asm) (header construction `SLMVC`/`HDRLNOK`, save +execution `SAMAIN`/`SVFL`, load execution `LDPRDT`/`LDPROG`) and +[tapex.asm](../tapex.asm) (block encoding `SABLK`/`LDBLK`). + +## Overall file structure + +Every SAM file is two blocks: + +| Block | Type byte | Length | Contents | +|---|---|---|---| +| Header | &01 | 80 bytes | The `HDR` buffer described below | +| Data | &FF | As given at header offset 34–36 | The file body | + +On **tape/net** the ROM writes the two blocks itself (each with its own +leader/sync/parity — see [Tape block encoding](#tape-block-encoding)). On a +**DOS device** the ROM builds the same 80-byte header at `HDR` (&4B00), +points IX at it, and issues hook &84 (`SVHK`) / &81 (`FOPHK`) — the DOS +stores header and data however it likes but must present the same header +back on load (the DOS re-enters the ROM's common code at `LDFL`/`LKTH`/ +`SVFL` with E = 1/3/2). + +## The 80-byte header + +Multi-byte *lengths and addresses* use the ROM's **page form**: three bytes +`pages, lo, hi` meaning \(\text{pages} \times 16384 + (\text{hi:lo} \bmod +16384)\) — the hi byte usually has bit 7 set (&8000-based section-C +address); loaders must mask bits 6–7 of the hi byte when forming the 14-bit +remainder, and treat the whole triple as invalid ("none") when the first +byte is &FF. + +| Offset | Size | Field | Contents | +|---|---|---|---| +| 0 | 1 | Type | 16 = BASIC program, 17 = numeric array, 18 = string array, 19 = CODE, 20 = SCREEN$ | +| 1–10 | 10 | Name | File name, space-padded (first byte &FF = null name = "match anything" on load) | +| 11–14 | 4 | Name extension | Extra name characters allowed when the device is not tape | +| 15 | 1 | Flags (`HFG`) | Bit 0 = invisible (name not printed while searching), bit 1 = protected | +| 16–26 | 11 | Type-specific | See below | +| 27 | 1 | `DIRE` | Directory entry number (request header only; unused by tape) | +| 28–30 | 3 | — | Spare | +| 31–33 | 3 | Start (`HDN`) | Data start, page form. BASIC: PROG's page and address (typically `00 D5 9C`). CODE: relative page form (relative to the calling context's base page; SAVE adds the current LRPORT so the stored value is absolute-ish). Arrays: address of the variable record, or &FFxxxx if it doesn't exist | +| 34–36 | 3 | Length | Data block length, page form (&FFxxxx in a *request* header means "whatever the file says", e.g. `LOAD "" CODE`) | +| 37–39 | 3 | Execute / auto-run | CODE: execution address (relative page form) or &FFxxxx = none. BASIC: byte 37 = 0 if there is an auto-run line, then bytes 38–39 = line number (lo,hi); byte 37 = &FF = no auto-run | +| 40–79 | 40 | Comment | Not initialised or interpreted by the ROM — free for tools to use | + +### Type-specific area (offsets 16–26) + +| Type | Offsets 16–26 | +|---|---| +| 16 BASIC | Three page-form lengths (see next section): 16–18 program alone, 19–21 program + numeric variables, 22–24 program + numeric variables + gap; 25–26 unused | +| 17 / 18 array | The variable's type/length byte (offset 16) followed by its 10-character name — exactly the 11 bytes of the variables-area record header | +| 19 CODE | Unused | +| 20 SCREEN$ | Offset 16 = screen MODE (0–3); rest unused | + +## What the data block contains for a BASIC program (type 16) + +SAVE writes **one contiguous block** starting at `PROG` with length +\((\text{ELINE} - 1) - \text{PROG}\) — that is, everything from the first +program line up to *but excluding* the &FF terminator that ends the +string/array area (the terminator is deliberately not saved; LOAD re-plants +one). The block therefore contains, in order: + +```text +PROG → program lines (tokenized, with &0E number forms and FN/PROC + calling buffers exactly as stored — see + tokenized-program-format.md), ending with the &FF program + terminator +NVARS → numeric variables: 26 letter-chain root pointers (52 bytes) + followed by the numeric variable records +NUMEND → the numbers-to-strings gap (saved as-is; usually a few hundred + slack bytes) +SAVARS → string and array records + → (the final &FF stopper is NOT in the file) +``` + +So a saved program carries its variables with it — `SAVE` after `RUN` +preserves state, and the pseudo-variables (XOS/YOS/XRG/YRG etc., which live +in the numeric variables area) travel too. + +The three lengths at header offsets 16/19/22 are the distances from PROG to +NVARS, NUMEND and SAVARS respectively, computed at save time +(`SUBAHLCDE` page-form subtraction of each pointer from PROG). + +### How LOAD reconstructs the program (`LDPRDT`/`LDPROG`) + +1. Check the file fits: compare the header length against free memory + (error 1 "Out of memory" if not). +2. Delete the current program *and* numeric variables in one reclaim + (NVARS is zeroed first so pointer auto-adjustment doesn't misfire), and + clear the BASIC stack (its return addresses are obsolete). +3. Open a block of exactly the file length at PROG (`MKRBIG`), plant a + provisional &FF at the start (so a failed load leaves an empty, + consistent program), and load the data block into it. +4. Rebuild the three region pointers by page-form addition: + NVARS = PROG + (hdr+16), NUMEND = PROG + (hdr+19), + SAVARS = PROG + (hdr+22). Everything above SAVARS (edit line onward) was + already in place around the opened block. +5. `RESTORE 0`, then set `COMPFLG` and run the compile pass (DOCOMP) — the + FN/PROC **calling-buffer addresses inside the loaded program are stale** + (they refer to wherever the program sat when it was saved) and are fully + recomputed here; a file written by an external tool may safely store its + buffers unresolved (`0E FE FE FF 00 00` form) for the same reason. +6. Auto-run: `LOAD "name" LINE n` overrides the header; otherwise if header + byte 37 = 0, GOTO the line at bytes 38–39. + +`MERGE` loads the same file into workspace instead, then splices: lines +replace same-numbered lines, numeric variables are re-created one by one, +strings/arrays replace same-named victims — using header lengths 16 and 22 +to find the section boundaries inside the loaded image. + +### Verification notes for tools + +* The data block must exactly match the pointers: program terminator &FF + present at PROG+(hdr16)−1… strictly, the &FF program stopper is the last + byte before NVARS; the 26 chain roots follow immediately. +* All numeric-chain pointers and calling buffers are position-independent + (relative links / recompiled), so a program image can be relocated freely + — only the header's three lengths and the total length must be + consistent. +* Line format inside the program section: + [tokenized-program-format.md §7](tokenized-program-format.md#7-the-final-in-memory-format-detokenizer-specification). + +## Other file types, briefly + +| Type | Data block | +|---|---| +| 17/18 array | The complete variables-area record **from its type/length byte**: 11-byte name header + 3-byte length + dimension data + elements (`ADD14` accounts for the 14 header bytes). LOAD deletes any same-named variable and splices the record into SAVARS, then overwrites the stored name with the requested one | +| 19 CODE | Raw bytes. Start/length/execute from the header; on load the start address is interpreted through the 0–512K `PDPSR2` mapping and the code optionally executed | +| 20 SCREEN$ | The screen bitmap for the saved MODE (&1B00 bytes mode 0, &3800 mode 1, &6000 modes 2/3) followed immediately by the 40-byte palette table (`PALTAB`) and, if present, the line-interrupt colour table (first byte &C3-terminated check) | + +## Tape block encoding + +`SABLK`/`LDBLK` ([tapex.asm](../tapex.asm)) implement a ZX-Spectrum-style +but **speed-programmable** format (`DEVICE Tn` sets the speed; default +`TSPEED` = 112; the half-cycle period scales from it, and the leader cycle +count is scaled inversely so leaders last roughly constant wall-clock +time): + +| Element | Encoding | +|---|---| +| Leader | A long run of uniform cycles (count scaled by speed) letting the loader measure the recording speed — the loader self-calibrates its bit threshold from it, so tapes recorded at any speed (including ZX speed) load | +| Sync | A distinctive short pulse pair marking the start of data | +| Type byte | First data byte: &01 = header block, &FF = data block (compared against the expected type; ZX 17-byte headers are recognised and translated — a ZX CODE header becomes a SAM type-19 header) | +| Data | Bytes MSB-first; each bit is one cycle, 0 short / 1 long relative to the measured threshold; the border stripes while saving/loading | +| Parity | Final byte: XOR of type byte and all data bytes; mismatch = "Loading error" (19) | + +`VERIFY` runs the same reception comparing against memory instead of +storing. Net transfers (`DEVICE N`) use the same two-block structure with +their own byte-level protocol (station addressing via `OTHER`, per-byte +parity). + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. diff --git a/docs/machine-code-interface.md b/docs/machine-code-interface.md new file mode 100644 index 0000000..b61f81c --- /dev/null +++ b/docs/machine-code-interface.md @@ -0,0 +1,303 @@ +# SAM Coupé ROM 3.0 — Machine-Code Interface + +How external (user) machine code interacts with the ROM: the public jump +table, the restarts, calling the floating-point calculator, and the +parameter-passing protocol for `CALL`, `USR` and `USR$`. + +Assumed context throughout: the paging BASIC gives to called code — ROM0 at +&0000 (section A), the system page at &4000 (section B), your code and data +in sections C/D. If your code changes LMPR (port 250), restore it before +using any of these services. + +## Restarts + +| RST | Name | Function | +|---|---|---| +| &00 | — | Reset (delay, ROM1 on, full initialisation) | +| &08 | Error | The byte following the RST is the error/hook code; control passes to the error handler (or DOS for codes ≥ 128). **Does not return** unless a DOS hook succeeds | +| &10 | Print | Print the character in A through the current channel | +| &13 | `PRINTSTR` | (CALL &0013) Print BC bytes from (DE) | +| &18 | Get char | A := significant character at (CHAD), spaces/control codes skipped, ROM1 paged out around the read | +| &20 | Next char | Advance CHAD, then as RST &18 | +| &28 | Calculator | Execute the FP-calculator code bytes that follow, until `EXIT` (&33). See [below](#calling-the-floating-point-calculator) | +| &30 | ROM1 link | From ROM0: call/jump into ROM1. **From RAM: vectors through `RST30V`** (&5AF2) — set that vector before using RST &30 in your own code | +| &38 | Interrupt | Maskable-interrupt entry; dispatches via `ANYIV` | + +## The jump table at &0100 + +Fixed public entry points, 3 bytes apart. "ROM" shows where the routine +body lives (calls are made to the table address regardless; the table +handles any paging). + +| Addr | Name | ROM | Contract | +|---|---|---|---| +| &0100 | `JSCRN` | 1 | Select screen C (1–16) as current output screen | +| &0103 | `JSVIN` | 0 | Call the routine whose address follows the CALL, with the system page mapped at &4000, ROM0 on/ROM1 off, on a private stack; everything restored on return | +| &0106 | `HEAPROOM` | 1 | Reserve BC bytes of heap (release if negative). Exit: DE = room start (old HEAPEND), HL = new HEAPEND, NC = not enough room (HL = shortfall) | +| &0109 | `WKROOM` | 0 | Open BC bytes at the workspace end. Exit: DE = start, HL = end, room paged in | +| &010C | `MKRBIG` | 0 | Open A×16K + BC bytes at (HL) (paged in), shifting everything above and adjusting all system pointers | +| &010F | `CALBAS` | 0 | Execute BASIC line HL (as a GOSUB from machine code). Exit: Z = OK, else A = error number | +| &0112 | `SETSTRM` | 0 | Select stream A (&FC–&0F… internally −4 to 15; &FC–&FF are the fixed system streams) as current channel | +| &0115 | `POMSG` | 0 | Print message number A from the bit-7-terminated list at DE | +| &0118 | `EXPT1NUM` | 0 | Evaluate a numeric expression at (CHAD); error if string. Result on the calculator stack if running | +| &011B | `EXPTSTR` | 0 | Evaluate a string expression at (CHAD) | +| &011E | `EXPTEXPR` | 0 | Evaluate either kind; Z = string, NZ = numeric; CY = running | +| &0121 | `GETINT` | 0 | Pop the calculator-stack top as an integer to BC (and HL, A=C). BASIC error 30 if negative or ≥ 65536 | +| &0124 | `STKFETCH` | 0 | Pop 5 bytes: A, E, D, C, B (for a string: A=page+flags, DE=start, BC=length) | +| &0127 | `STKSTORE` | 0 | Push 5 bytes A, E, D, C, B onto the calculator stack | +| &012A | `SBUFFET` | 0 | Pop a string descriptor and copy the text to `INSTBUF` (&4F00). Error if length 0 or > 255. Exit: DE = start, BC = A = length | +| &012D | `FARLDIR` | 0 | Copy (PAGCOUNT)×16K + (MODCOUNT) bytes from page A, addr HL to page C, addr DE, ascending, across any page boundaries | +| &0130 | `FARLDDR` | 0 | As FARLDIR, descending (for overlapping upward moves) | +| &0133 | `JPUT` | 0 | PUT block: A = mode 0–5, B = Y, C = X, HL → width/len-prefixed data, HL' → mask | +| &0136 | `JGRAB` | 0 | GRAB block: D=len, E=width(bytes), B=Y, C=X. Exit: DE=start, BC=len of captured data | +| &0139 | `JPLOT` | 0 | Plot: coordinates in C,B (fat) or HL,B (thin) | +| &013C | `JDRAW` | 0 | Draw relative: registers as the DRAW internals (B=Y diff, C/HL=X diff, D/E = sign bytes) | +| &013F | `JDRAWTO` | 0 | Draw to absolute X,Y in C,B / HL,B | +| &0142 | `JCIRCLE` | 0 | Circle: A = radius, C,B = centre, HL = thin-pixel offset | +| &0145 | `JFILL` | 0 | Fill: HL = coords; DE = pattern (0 = solid); A = 0 to build the check screen | +| &0148 | `JBLITZ` | 0 | Execute a BLITZ graphics string: DE = data, BC = length | +| &014B | `JROLL` | 0 | Roll/scroll: B=pixels, C=direction 1–4, HL=top-left coords, D=len, E=width, A=&FF roll/0 scroll | +| &014E | `CLSBL` | 0 | Clear whole screen if A=0, else clear window | +| &0151 | `CLSLOWER` | 0 | Clear the lower screen window, select channel K | +| &0154 | `JPALET` | 1 | Palette set: A = line (&FF if none), B/C = colours, E = palette entry | +| &0157 | `JOPSCR` | 1 | Open screen: B = mode, C = screen number | +| &015A | `MODET` (`MODPT2`) | 1 | Set MODE A (0–3), windows, expansion tables; clears the screen | +| &015D | `JTCOPY` | 1 | Text COPY (via `DMPV` vector — no-op unless a dump driver is installed) | +| &0160 | `JGCOPY` | 1 | Graphics COPY (via `DMPV`) | +| &0163 | `RECLAIM2` | 0 | Close up BC bytes at (HL), adjusting all system pointers | +| &0166 | `KBFLUSH` | 0 | Flush the keyboard queue | +| &0169 | `READKEY` | 0 | Read the keyboard (INKEY$ style). Exit: CY,NZ with A = key, or Z,NC = none | +| &016C | `KYIP2` | 0 | Fetch a queued key: CY with A = key, Z,NC = none (no waiting) | +| &016F | `BEEPP2` | 1 | Sound: DE−1 cycles at period HL (8-T units) | +| &0172 | `SABYTES` | 1 | Save block to tape/net: A = type (1 header/&FF data), HL = start (paged in), CDE = length (page form) | +| &0175 | `LDBYTES` | 1 | Load (CY) or verify (NC) a block: A = expected type, HL = dest, CDE = length | +| &0178 | `JLDVD` (`LDVD2`) | 1 | Load/verify CDE bytes at HL from the current device (tape/net/DOS) | +| &017B | `EDGE2` | 0 | Tape edge timer: C = pulse length, CY = edge found, NC = BREAK/timeout | +| &017E | `JPFSTRS` (`PFSTRS`) | 1 | Convert the calculator-stack top to text: BC digits at (DE) in the print buffer | +| &0181 | `SENDA` (`SNDA2`) | 1 | Send byte A to the printer port | +| &0184 | `IMSCSR` | 0 | SCREEN$ core: DE = line/col. Exit CY: not recognised; NC: (HL) = char, BC = 1 | +| &0187 | `GRCOMP` | 0 | Compress a mode 2/3 character cell to 1-bit form (graphics COPY support) | +| &018A | `JGTTOK` (`GETTOKEN`) | 1 | Keyword match: HL = word list −1 (bit-7-terminated words), A = word count +1, DE = candidate text. Exit: A = 1-based match index, Z = no match | +| &018D | `JCLSCR` | 1 | Close screen C | + +## Calling the floating-point calculator + +The calculator is a byte-coded stack machine executed by `RST &28`. The +bytes *following the RST in your own code* are its program; execution +resumes after the terminating `EXIT` byte. It works from RAM exactly as +from ROM (the handler makes IX the instruction pointer via `EX (SP),IX`, +pages ROM1 in for the duration, and restores everything on exit). + +### The stack + +* Entries are **5 bytes** each; `STKEND` (&5C65) points at the first free + byte; the stack base is `FPSBOT` (&4D00). Roughly 50 entries fit before + the machine-stack region — plenty. +* Number forms (details in + [tokenized-program-format.md §9](tokenized-program-format.md#9-the-5-byte-number-format)): + small integer `00 sign lo hi 00`, or FP `exponent m1 m2 m3 m4` where the + magnitude is \(m \times 2^{e-128}\) with \(0.5 \le m < 1\), the mantissa's + leading 1 implicit, and its bit 7 replaced by the sign. +* Strings on the same stack are 5-byte descriptors: + `page+flags, start-lo, start-hi, len-lo, len-hi`. + +### Pushing operands + +Use `STKSTORE` (&0127), which writes A, E, D, C, B in that order: + +| To push | A | E | D | C | B | +|---|---|---|---|---|---| +| Integer n (0–65535) | 0 | 0 | n lo | n hi | 0 | +| Integer −n | 0 | &FF | (−n) lo | (−n) hi (two's complement) | 0 | +| Full FP value | exponent | mantissa 1 | mantissa 2 | mantissa 3 | mantissa 4 | +| String | page | start lo | start hi | len lo | len hi | + +Alternatively copy 5 bytes to (`STKEND`) yourself and add 5 to `STKEND`. + +### Running a calculation + +Example — hypotenuse \(\sqrt{x^2+y^2}\) of the two numbers already on the +stack (x below y): + +```z80 + RST &28 + DB &25 ; DUP x, y, y + DB &00 ; MULT x, y*y + DB &06 ; SWOP y*y, x + DB &25 ; DUP y*y, x, x + DB &00 ; MULT y*y, x*x + DB &01 ; ADDN y*y + x*x + DB &43 ; SQR result + DB &33 ; EXIT +``` + +The complete operation set is in +[constants.md](constants.md#fpcmainasm): binary ops &00–&1F, control/stack +ops &20–&34 (jumps take a signed displacement byte; `ONELIT`/`FIVELIT` +take inline literals), functions &39–&5D, memory store/recall &C8–&DD, +constants &E0–&F0. Notes: + +* **B register**: the B you hold at the `RST &28` is captured into `BCREG` + and is what `STKBREG` (&23), `LDBREG` (&21), `DECB` (&22) and `USEB` + (&24) operate on. BC is restored on exit. +* **Memories**: `STO`/`RCL` 0–5 use the area pointed to by `MEM` (&5C68) — + by default the six slots at `MEMVAL` (&5121). If BASIC might be using + them (FOR/NEXT does), point `MEM` at 30 bytes of your own first. +* **Termination**: always end with `EXIT` (&33). `EXIT2` (&34) additionally + unwinds the RST frame and is only meaningful in ROM-internal contexts — + do not use it from your own code. +* **Errors**: overflow, "FPC error" etc. execute `RST &08` and long-jump to + BASIC's error handler via `ERRSP` — your code after the RST will *not* + regain control. If you need to survive errors, push an error frame first + (`SETESP` idiom: save `ERRSP`, point it at your own SP with a recovery + address on the stack) or pre-validate ranges. +* The `RST28V` vector (&5AF0) sees every operation byte before dispatch, + for extending the instruction set. + +### Reading the result + +* `GETINT` (&0121): BC/HL = integer (raises BASIC error 30 if negative or + out of range — see the error caveat above). +* `STKFETCH` (&0124): pops the raw 5 bytes into A,E,D,C,B — safe for any + value; decode the forms yourself. +* Or read the 5 bytes at (`STKEND`)−5 and subtract 5 from `STKEND`. + +## CALL, USR and USR$: parameters and results + +### How the start address is passed and mapped + +All three keywords take a numeric expression as the start address, evaluate +it onto the calculator stack, and hand it to the shared trampoline `CALLX` +(eval.asm), which pops it and resolves it through `PDPSUBR` (misc1.asm) — +the same 0–524287 address scheme used by PEEK/DPEEK/POKE. The value is +interpreted **relative to the context base page** (page 0 for a normal +BASIC program), and the ROM pages the target in for you: + +| Address argument \(N\) | Executes at | Paging while your code runs | +|---|---|---| +| 0 – 16383 | &0000 + N | ROM0 (section A) — i.e. you can CALL ROM0 routines directly | +| 16384 – 32767 | &4000 + (N − 16384) | The base (system) page, section B | +| 32768 – 49151 | &8000 + (N − 32768) | Page base+1 selected into section C | +| 49152 – 65535 | &C000 + (N − 49152) | URPORT = base+1, so the code runs in base+2 via section D | +| ≥ 65536 | &8000 + (N mod 16384) | Page base + \(\lfloor N/16384 \rfloor\) − 1 selected into section C | + +(That last row is why the ReadMe's `CALL 229385` reaches offset 9 of RAM +page 13 on a 256K machine: \(229385 = 14 \times 16384 + 9\).) + +**Register state on entry to your routine** (`CALLX`): + +| Register | Contents | +|---|---| +| HL | The mapped entry address — i.e. the address your code is running at | +| BC | A copy of the same entry address | +| A | `CALL` only: the number of parameters (from `TEMPB3`); junk for USR/USR$ | +| IX | Saved by the ROM and restored after you return | +| SP | The normal machine stack; a plain `RET` returns to the ROM | + +The original URPORT (section C/D paging) is restored after your `RET`, so +you may repage freely. So the start address is not "passed in" as data you +must decode — it *is* your execution address, pre-mapped, and it is also +handed to you in HL and BC (useful for position-independent code that wants +to know where it is; note that for `USR` this means **BC arrives holding +the entry address and must be overwritten with your result**). + +### How CALL passes parameters + +`CALL address, p1, p2, …, pn` (misc31.asm `CALLER`) evaluates the address, +then each parameter left to right. After each parameter it also stacks a +**type entry**, keeping the address on top; the address is consumed by the +paging code just before your routine is entered. On entry to your code: + +* **A** = number of parameters (0–15; also in `TEMPB3`). +* The calculator stack holds, from bottom to top: + +```text +value(p1) type(p1) value(p2) type(p2) … value(pn) type(pn) + ↑ top (STKEND) +``` + +* **IX** is saved/restored by the ROM; all other registers are yours. + Return with `RET` (the stack is otherwise clean); execution continues at + the next statement. + +**Type entries** are 5-byte small integers whose value is the `FLAGS` byte +captured after evaluating that parameter. The only meaningful bit is +**bit 6: 1 = numeric, 0 = string** (other bits — e.g. bit 7 "running" — are +incidental and should be masked off). + +**Value entries** are either a 5-byte number (integer or FP form) or, for +strings, a descriptor `page+flags, start-lo, start-hi, len-lo, len-hi`. +Bit 7 of the page byte is an internal "delete after use" flag — mask the +page with &1F. The address is in the &8000–&BFFF window of that page; if +`start+len` crosses &C000, continue in the next page. + +### Retrieving them + +Pop from the top, i.e. **parameters come off in reverse order** (pn first), +alternating type/value. For each parameter: + +```z80 + CALL &0124 ; STKFETCH -> type entry: D = FLAGS value + BIT 6,D + JR Z,is_string + + CALL &0124 ; numeric: A,E,D,C,B = the 5-byte number + ; (or CALL &0121 GETINT if you want an integer in BC, + ; but note it can raise BASIC error 30) + JR next_param + +is_string: + CALL &0124 ; A = page+flags, DE = start, BC = length + AND &1F ; usable page number + ; page it into section C: keep URPORT's top bits + LD L,A + IN A,(251) + AND &E0 + OR L + OUT (251),A ; string text now at DE, length BC +``` + +(`SBUFFET`, &012A, is a convenient alternative for strings ≤ 255 bytes: it +copies the text into `INSTBUF` at &4F00 for you.) + +**Consume all 2n entries.** CALL provides no result channel and does not +clean the stack for you; leftovers linger until the program ends (they +won't crash anything, but they leak calculator-stack space and `STKEND` +must be balanced if BASIC is to keep evaluating expressions correctly). + +### Returning values + +`CALL` itself cannot return a value — a routine that must hand results back +should either write them into memory the BASIC program can `PEEK`/`DPEEK` +(or into a pre-dimensioned string/array whose address was passed as a +parameter — write through the passed descriptor), or be invoked as a +function instead: + +* **`USR address`** — numeric result. The ROM arranges the return so that + whatever your routine leaves in **BC** is stacked as the function's value + (0–65535). Remember that BC arrives holding the entry address (see + [above](#how-the-start-address-is-passed-and-mapped)), so a routine that + returns nothing meaningful still returns *something* — load BC + deliberately. Type is implicitly numeric; you cannot signal anything else. +* **`USR$ address`** — string result. On your `RET`, the ROM stacks + **A = page, DE = start, BC = length** as a string descriptor (`STKSTOS` + also clears FLAGS bit 6, marking the expression result as a string). The + text must be somewhere stable — workspace obtained via `WKROOM` (&0109) + is the intended place. + +USR/USR$ receive **no data parameters**: their single numeric argument is +the start address itself, consumed by the mapping described +[above](#how-the-start-address-is-passed-and-mapped) (it reaches your code +only as the entry address in HL/BC — nothing is left on the calculator +stack). Pass data to them through memory, or use CALL when you need the +full parameter machinery and results via memory. + +Type information, in both directions, is therefore: *incoming* — the +explicit type entries on the calculator stack (bit 6 of the captured FLAGS +value); *outgoing* — determined by the keyword used (`USR` numeric, `USR$` +string), never by the machine code at run time. + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. diff --git a/docs/memory-map.md b/docs/memory-map.md new file mode 100644 index 0000000..f66aecb --- /dev/null +++ b/docs/memory-map.md @@ -0,0 +1,88 @@ +# SAM Coupé ROM 3.0 — Memory Map + +Derived from the `EQU` values in [vars.asm](../vars.asm), the `MAIT`/`CHIT` +initialisation tables in [text.asm](../text.asm), and the initialisation code +in [misc31.asm](../misc31.asm) (`MNINIT`). + +## Address spaces + +The Z80's 64K space is four 16K sections, each independently pageable: + +| Section | Address range | Normal contents while BASIC runs | +|---|---|---| +| A | &0000 – &3FFF | **ROM0** (paged out only for special cases) | +| B | &4000 – &7FFF | **The system page — physical RAM page 0** (LMPR low bits = &1F) | +| C | &8000 – &BFFF | Working page: BASIC program/variables page, screen, or whatever URPORT selects | +| D | &C000 – &FFFF | Page above section C's, or **ROM1** when its enable bit is set | + +All system variables and fixed buffers below are in physical **page 0**, +addressed in section B. The BASIC program area *starts in the same page* +(immediately after the channels) but is addressed through section C — BASIC +addresses it as page 0 at &9CD5 and lets it grow across pages 1, 2, … up to +RAMTOP. + +## System page (physical page 0) + +Addresses are as seen in section B (&4000–&7FFF); the "offset" column is the +position within the 16K page. Length is in bytes. + +| Start | End | Length | Offset | Region | Description | +|---|---|---|---|---|---| +| &4000 | &4AFF | 2816 | &0000–&0AFF | Heap + BASIC stack | Heap (`HPST`/`HEAPEND`) grows **up** from &4000 (initially empty); the DO/GOSUB/PROC "BASIC stack" (`BSTKEND`) grows **down** from `BSTACK` = &4AFF. The gap between `HEAPEND` and `BSTKEND` is **free** (error 41 when they meet) | +| &4B00 | &4B4F | 80 | &0B00 | `HDR` | Save/load header being requested/built; also the PROC rename stack (nparpro.asm) | +| &4B50 | &4B9F | 80 | &0B50 | `HDL` | Header loaded from tape/disc/net | +| &4BA0 | &4BFF | 96 | &0BA0 | *(spare)* | Unnamed; headroom for the interrupt stack (the source notes `INTSTK` "uses down to ~&49EE" in the deepest cases — tape I/O runs with interrupts off, so HDR/HDL are safe) | +| &4C00 | &4CFF | 256 | &0C00 | `BUFF256` / `INTSTK` | 256-byte bounce buffer for FARLDIR page-to-page copies; the interrupt stack also grows **down** from &4C00 | +| &4D00 | &4E80 | 385 | &0D00 | `FPSB` / `CDBUFF` | Bottom of the FP calculator stack (grows **up** from &4D00, top tracked by `STKEND`); overlaid by `CDBUFF`, the generated-code buffer (multi-LDI/RLD runs, max &181 bytes) — the RAM copy of the tokenizer runs at `CDBUFF+&80` (&4D80–&4E24) | +| &4E81 | &4EFF | 127 | &0E81 | Machine stack | Grows **down** from `ISPVAL` = &4F00 ("used down to ~&4E98 normally") | +| &4F00 | &50FF | 512 | &0F00 | `INSTBUF` | Execution buffer for command bodies copied from ROM1 (RENUM, GET, DELETE, POP, INPUT, DEF KEYCODE, DEF FN, MERGE), filename/SOUND/INSTR scratch. Overlaid sub-buffers: `FILBUFF` &5080 (128, FILL pattern) and `MSGBUFF` &50C0 (64, message assembly) | +| &5100 | &5120 | 33 | &1100 | `ALLOCT` | Page allocation table: 1 byte per RAM page (0=free, &40=reserved, &60=DOS, &C0=screen, &FF=absent/terminator) | +| &5121 | &513E | 30 | &1121 | `MEMVAL` | The six 5-byte calculator memories (default target of `MEM`) | +| &513F | &513F | 1 | &113F | `TLBYTE` | Type/length byte of the variable name being processed | +| &5140 | &5187 | 72 | &1140 | `NMBUFF` / `FIRLET` | Variable/FN/PROC name buffer (a second name copy lives at `TLBYTE+33` = &5160) | +| &5188 | &518F | 8 | &1188 | `SCRNBUF` / `NMISTK` | SCREEN$ compressed-character buffer; the NMI stack grows **down** from &5188 | +| &5190 | &55D7 | 1096 | &1190 | `CHARSVAL` | Unpacked character set, 8 bytes/char for codes 32–168 (`CHARS` = &5090 = CHARSVAL−256; `UDG` = &5510 = CHR$ 144) | +| &55D8 | &55FF | 40 | &15D8 | `PALTAB` | Working palette: 2 × 16 palette memories (flash pair) + mode-2 spare entries | +| &5600 | &57FF | 512 | &1600 | `LINICOLS` | Line-interrupt colour table (up to 127 × 4-byte entries + terminator) | +| &5800 | &5880 | 129 | &1800 | `DKBU` | DEF KEY definitions (`DKDEF`; growth limit `DKLIM` = &5880 by default) | +| &5881 | &58DF | 95 | &1881 | *(spare)* | Unused between the DEF KEY limit and the key map | +| &58E0 | &59FF | 288 | &18E0 | `KTAB` | Keyboard translation map: 3 × 70-entry planes (normal/caps/symbol) + control-key values | +| &5A00 | &5BFF | 512 | &1A00 | `VAR2` | Main system-variable block (see [constants.md](constants.md#varsasm); ends with the 18-byte `KBUFF` at &5BEE) | +| &5C00 | &5CB5 | 182 | &1C00 | ZX-style sysvars | `LHM1`…`PRAMTP`: keyboard state, `STREAMS`, `FLAGS`, `PPC`, `CHANS`, `STKEND`, `RAMTOP` etc. (small documented spare gaps inside) | +| &5CB6 | &5CD4 | 31 | &1CB6 | Channels | Six 5-byte channel records (output addr, input addr, letter: K, S, R, P, $, B) + &0D terminator; `CHANS` points here | +| &5CD5 | &7FFF | 9003 | &1CD5 | **Start of BASIC program area** | `PROG` = page 0, address &9CD5 (section C view). The program, then the numeric variables, gap, strings/arrays, edit line, and workspace continue from here across pages 1, 2, … | + +Totals: &4000–&5CD4 (7381 bytes) of fixed system use; the rest of page 0 +belongs to the moving BASIC area. + +## The moving BASIC area (section C view, page 0 onward) + +These regions are contiguous and slide as MAKEROOM/RECLAIM insert and delete +bytes; each boundary is a (page, address) system-variable pair. Order, low to +high: + +| Pointer | Region that follows it | Contents | +|---|---|---| +| `PROG` (= 0:&9CD5) | Program | Tokenized program lines, terminated by &FF | +| `NVARS` | Numeric variables | 26 letter-chain root pointers (52 bytes) + variable records | +| `NUMEND` | Gap | Free slack between numbers and strings (new numeric variables grow into it; re-opened 512 bytes at a time) | +| `SAVARS` | Strings & arrays | String/array records, terminated by &FF | +| `ELINE` | Edit line | The line being typed/edited, ending &0D &FF | +| `WORKSP` | Workspace | INPUT data, temporary strings | +| `WKEND` | *(free)* | Free memory up to `RAMTOP` | +| `RAMTOP` | — | Default = &BFFF in page `LASTPAGE`; above it: user reserved pages and the screen pages at the top of RAM (2 pages per open screen) | + +Not in this chain (fixed in the system page): the FP calculator stack +(&4D00, `STKEND`), the heap, the BASIC stack, and the machine stack. + +## ROM layout + +| Start | End | Length | Contents | +|---|---|---|---| +| &0000 | &3FFF | 16384 | ROM0 — restarts, [jump table at &0100](machine-code-interface.md#the-jump-table-at-0100), editor, interpreter loop, evaluator, variables, graphics (see [source-files.md](source-files.md)) | +| &C000 | &FFFF | 16384 | ROM1 — FP calculator/arithmetic, printing, tape/net, screens/interrupts, keyword & message tables (paged over section D on demand; the paging bit lives in LRPORT bit 6, toggled by the RST &30 mechanism) | + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. diff --git a/docs/source-files.md b/docs/source-files.md new file mode 100644 index 0000000..1be60f6 --- /dev/null +++ b/docs/source-files.md @@ -0,0 +1,2398 @@ +# SAM Coupé ROM 3.0 — Source File and Routine Reference + +The ROM is built by [samrom.asm](../samrom.asm), which `include`s the modules +below in order. The first group assembles into **ROM0** (&0000–&3FFF, always +visible at address 0); the second into **ROM1** (assembled at &C000, paged into +the top 16K). ROM0 holds the restarts, editor, main interpreter loop, +expression evaluator front-end, variable handling and graphics primitives; +ROM1 holds the floating-point calculator, arithmetic, printing, tape/net I/O, +and the keyword/message tables. Code in ROM1 that must run while the BASIC +program is paged into the same address range is copied into RAM buffers +(`INSTBUF`/`CDBUFF`) before execution. + +Companion documents: + +- [constants.md](constants.md) — every `EQU` constant, by file. +- [tokenized-program-format.md](tokenized-program-format.md) — the tokenizer, + the compile pass, and the exact in-memory form of a tokenized program. +- [memory-map.md](memory-map.md) — the system-page and BASIC-area memory map. +- [machine-code-interface.md](machine-code-interface.md) — the jump table, + restarts, calling the FP calculator, and CALL/USR parameter passing. +- [file-formats.md](file-formats.md) — the saved-file header and how a BASIC + program is encoded on tape/disc. + +## File index + +| File | Contents | +|---|---| +| [main.asm](#mainasm) | ROM0 restarts (RST 0–&38), NMI handler, public jump table at &0100, ROM0↔ROM1 paging call helpers | +| [vars.asm](#varsasm) | System variable definitions only (no code) — see [constants.md](constants.md#varsasm) | +| [editor.asm](#editorasm) | Line editor: key dispatch, cursor movement, insert/delete, DEF KEY expansion | +| [list.asm](#listasm) | AUTOLIST, LIST/LLIST, CLS family, PRINT command, cursor up/down through the program | +| [roll.asm](#rollasm) | ROLL and SCROLL commands, window clear, editor scroll, pixel-row address stepping | +| [mainlp.asm](#mainlpasm) | The interpreter main loop: LINESCAN syntax pass, statement dispatch, line insertion, error handling | +| [misc1.asm](#misc1asm) | Streams/channels (SETSTRM), temporary colour vars (TEMPS), READ, POKE/DPOKE, colour items | +| [lookvar.asm](#lookvarasm) | Variable lookup: name parsing, numeric variable chains, string/array search | +| [eval.asm](#evalasm) | Expression evaluator (SCANNING/SCANSR), operator priorities, literal number conversion (CALC5BY) | +| [do.asm](#doasm) | DO/LOOP, IF/ELSE/END IF, FOR/NEXT, GOTO/GOSUB/RETURN plumbing, BASIC stack, FNDLINE, ON ERROR | +| [tadjm.asm](#tadjmasm) | FP-stack helpers (stack/fetch/GETINT), program search (SEARCHALL), MAKEROOM/RECLAIM, page-form arithmetic | +| [graph0.asm](#graph0asm) | CIRCLE and DRAW (line drawing, thin-pixel draw) | +| [graph1.asm](#graph1asm) | PLOT and the per-mode pixel plotting subroutines; SETIY plot dispatcher | +| [graph2.asm](#graph2asm) | BLITZ (graphics string interpreter), FILL, screen→check-screen transfer, coordinate fiddling | +| [grabput.asm](#grabputasm) | GRAB and PUT block graphics; FARLDIR/FARLDDR cross-page block moves | +| [assign.asm](#assignasm) | Assignment (ASSIGN/STKVAR), DIM, string slicing, variable creation | +| [fn.asm](#fnasm) | DEF FN/FN evaluation, DEF PROC/PROC call, LOCAL, the COMPILE pass that patches call buffers | +| [nparpro.asm](#nparproasm) | PROC parameter processing (by value and by REF), RESTORE, local-variable teardown | +| [misc2.asm](#misc2asm) | RST 8 error entry and DOS hand-off, ROM1→RAM stub loaders (incl. TOKMAIN), LET/DEFAULT, RUN/CLEAR, syntax helpers, class checks | +| [endprint.asm](#endprintasm) | Final per-mode character rendering, screen address calculation, string compare, buffer fetch, memory-room tests | +| [miscx1.asm](#miscx1asm) | ROM1 bodies copied to RAM: RENUM, GET, DELETE, KEYIN, POP, INPUT | +| [miscx2.asm](#miscx2asm) | More copied bodies: DEF KEYCODE, DEF FN statement, **the tokenizer (TOKPT2)**, MERGE | +| [fpcmain.asm](#fpcmainasm) | Floating-point calculator: opcode dispatch table, control loop, literals, jumps, comparisons | +| [transend.asm](#transendasm) | Transcendental functions: SIN/COS/TAN/ASN/ACS/ATN/LN/EXP/POWER, Chebyshev SERIES generator | +| [mult.asm](#multasm) | FP multiply/divide/add/subtract, integer fast paths, number form conversion | +| [rom1fns.asm](#rom1fnsasm) | VAL/VAL$, RND, ATTR, POINT, INKEY$, CHR$, BIN$/HEX$, MEM$, CONCAT, &-hex literals, PEEK/DPEEK, STR$, CODE | +| [scrsel1.asm](#scrsel1asm) | OPEN/CLOSE SCREEN and streams, page allocation, all interrupt service (frame/line/MIDI/mouse), keyboard scan buffering | +| [scrsel2.asm](#scrsel2asm) | GOTO/GOSUB (+ ON), **GETTOKEN keyword matcher**, MODE, FATPIX, CSIZE, AUTO, SOUND, BOOT | +| [printfp.asm](#printfpasm) | Number-to-text conversion (STR$/PRINT of numbers), power-of-ten scaling | +| [tprint.asm](#tprintasm) | Character/token output: ASCII, UDGs, keyword expansion, control codes, scroll prompts, number printing | +| [tapemn.asm](#tapemnasm) | SAVE/LOAD/MERGE/VERIFY command parsing and execution, header handling | +| [tapex.asm](#tapexasm) | Tape/net low-level block save/load, edge timing, net/MIDI byte I/O | +| [using.asm](#usingasm) | XOINTERS pointer adjustment after MAKEROOM/RECLAIM, heap, INSTR, LENGTH, STRING$, code-buffer builders (CRTBF), DRAW curve | +| [misc31.asm](#misc31asm) | CALL, machine init (MNINIT), NEW, charset unpacker, PALETTE command and line-interrupt colour table | +| [misc32.asm](#misc32asm) | BEEP and sound effects (ZAP/POW/BOOM/ZOOM), KEY, DEVICE, PAUSE, colour-item execution, BORDER, WINDOW, OUT, STOP, RANDOMIZE | +| [scrfn.asm](#scrfnasm) | COPY, SCREEN$ recognition, **OUTLINE — the listing/detokenizing routine**, pretty-list indenting, cursor output, number printing | +| [text.asm](#textasm) | Data: error messages (compressed), keyword table, key maps, command address table, initial palette, compressed charset | +| [romtest.asm](#romtestasm) | Stand-alone test program comparing the assembled image against the live ROM (not part of the ROM build) | + +--- + +## main.asm + +The very bottom of ROM0: the Z80 restart vectors, interrupt entries, and the +public jump table at &0100 that gives external code stable entry points into +both ROMs. Also contains the RST &30 mechanism used throughout the ROM to call +routines in the *other* ROM with automatic paging. + +| Entry point | Description | +|---|---| +| [L0000](#l0000) | Reset: DI and jump to `MINITH` (delay, enable ROM1, jump to `MNINIT`) | +| `HLJUMP`/`IYJUMP`/`IXJUMP`/`BCJUMP` | Tiny `JP (rr)` helpers used as computed-jump primitives | +| [NRWRITE](#nrwrite) | `LD (HL),A : RET` — write helper callable with ROM paged appropriately | +| RST &08 | Error restart: byte after the RST is the error code; jumps to `ERROR2` in misc2.asm | +| RST &10 ([RST102](#rst102)) | Print character in A through the current channel | +| [PRINTSTR](#printstr) (&0013) | Print BC bytes from (DE) via the channel's string output (`SOP2`) | +| RST &18 ([GETCHAR](#getchar)) | Get the character at CHAD, skipping spaces and control codes (not CR), with ROM1 paged out | +| RST &20 ([NEXTCHAR](#getchar)) | Advance CHAD then behave as RST &18 | +| RST &28 ([FPCP2](#fpcp2)) | Floating-point calculator entry — pages in ROM1 and runs `FPCMAIN` over the inline code bytes | +| RST &30 ([RST30L2](#rst30l2)) | Call/jump into ROM1: the word after the RST is the target −&8000; bit 15 clear means "jump" instead of "call" | +| RST &38 | Maskable interrupt: reads STATPORT, pages system page in, dispatches via `ANYIV` (normally [ANYI](#anyi)) | +| NMI (&0066) | Pages to a safe state, calls the `NMIV` vector (normally super-break), restores paging | +| [RDCN](#rdcn)/[NUMBER](#rdcn) | Read char at (HL) / skip an invisible &0E+5-byte number form (adds 6 to L) | +| `NRREAD`/`RDDE` | `LD A,(HL)` / `LD A,(DE)` with RET — read helpers for cross-ROM peeking | +| [NXCHAR](#nxchar) (&0074) | Advance CHAD and read the next char with *no* skipping (used inside number parsing) | +| [Jump table &0100](#jump-table) | Public vectors: JSCRN, HEAPROOM, WKROOM, MKRBIG, CALBAS, SETSTRM, POMSG, EXPT1NUM…, GETINT, STKFETCH/STKSTORE, SBUFFET, FARLDIR/FARLDDR, JPUT/JGRAB/JPLOT/JDRAW/JDRAWTO/JCIRCLE/JFILL/JBLITZ/JROLL, CLSBL/CLSLOWER, palette/screen/copy hooks, RECLAIM2, KBFLUSH, READKEY, KYIP2, BEEP, SABYTES/LDBYTES/LDVD2, EDGE2, PFSTRS, SENDA, IMSCSR, GRCOMP, GETTOKEN, JCLSCR | +| [MODECMD](#modecmd) | The MODE command: insist on a number 1–4, then jump to `MODPT2` in ROM1 | +| [S16OP](#s16op) | Channel `$` output stub — calls `S16OSR` (append char to a string variable) with ROM1 off | +| [R1OFFCL / R1OFFJP / R1OFFCLBC](#r1offcl) | Call (or jump to) the address in the following word/BC with ROM1 paged out, restoring paging afterwards | +| [R1XJP](#r1xjp) | Turn ROM1 off permanently and jump to (BC) | +| [SOP2](#sop2) | String output: uses the channel's block-output entry if it has one, else repeated RST &10 | +| [JSVIN](#jsvin) | "Jump with system variables in": pages the system page in at &4000 with a private stack, calls the parameter address, restores everything | +| `STRMTAB` | Initial stream displacement table for streams −5…3 | + +### Details + +#### L0000 +Reset entry. Interrupts are disabled and control passes to `MINITH`, which +runs a ~300 ms delay loop (waiting for the ASIC to become ready — this delay +is what the pre-production ROMs lacked), switches ROM1 on via port 250, and +jumps to `MNINIT` in misc31.asm for the full machine initialisation. + +#### RST102 +The RST &10 print restart. Saves IX/HL/DE/BC, fetches the output-routine +address from the current channel (`CURCHL`) and calls it. Every character +printed by the ROM funnels through here; the channel address is temporarily +redirected to implement control-code parameter collection. + +#### PRINTSTR +Address &0013. Prints BC characters starting at (DE). `SOP2` implements it: +if the current channel provides a special string-output entry (flagged by a +&40 byte in the channel record), the whole block is handed over in one call; +otherwise it loops on RST &10. DE ends just past the string, BC ends 0. + +#### GETCHAR +RST &18 / RST &20 — the interpreter's character fetch. RST &18 loads HL from +CHAD, RST &20 pre-increments CHAD. Both then page ROM1 *out* (so the program +area at &C000 is visible), skip every byte below &21 except CR (updating CHAD +past skipped spaces/control bytes), restore the previous paging and return the +significant character in A. This is why spaces are insignificant almost +everywhere in SAM BASIC source lines. + +#### FPCP2 +RST &28. Saves the caller's IX and makes IX point at the byte after the RST — +the calculator's instruction pointer. Saves BC in `BCREG`, forces ROM1 on, +and calls `FPCMAIN`. The calculator executes the byte codes following the RST +until an `EXIT`/`EXIT2` code, then execution resumes after them with paging +restored. See [fpcmain.asm](#fpcmainasm). + +#### RST30L2 +RST &30 — the inter-ROM linkage. When executed from within ROM0, the word +following the RST is a ROM1 address minus &8000: bit 15 set means CALL, clear +means JUMP (bit 7 of the stored high byte is used as the flag and set before +use). ROM1 is paged in around the call and the original paging restored. +When RST &30 is executed from RAM (address ≥ &4000) it re-dispatches through +the `RST30V` vector instead, so user code can use its own convention. + +#### ANYI +The default RST &38 handler body (via `ANYIV`). The interrupt stub reads +STATPORT and LRPORT, forces both ROMs on with page 0 in section B, then `ANYI` +switches to the dedicated `INTSTK` stack and calls `INTS` (scrsel1.asm) which +demultiplexes frame/line/MIDI/mouse interrupt sources. + +#### RDCN +`RDCN` = `LD A,(HL)` then fall into `NUMBER`: if A is the &0E number marker, +add 6 to the pointer (skipping the marker and the 5-byte numeric form) and +return the following character. This is the canonical "skip invisible number" +primitive used by the lister, searchers and skippers. + +#### NXCHAR +&0074. Increment CHAD and return the byte there with **no** skipping — used +by the number parser, which must see every digit and cannot have spaces +skipped for it. + +#### Jump table +At &0100 sits a table of `JP`/`RST &30` entries providing a stable public API +(addresses &0100, &0103, &0106 … &018D). Comments in the source give each +vector's contract; notable ones: &010F `CALBAS` (call a BASIC line from +machine code), &0112 `SETSTRM`, &0118/&011B/&011E expression evaluation, +&0121 `GETINT`, &0124/&0127 string stack fetch/store, &012D/&0130 +`FARLDIR`/`FARLDDR` (cross-page block copy), the graphics entries +(&0133–&014B), &0163 `RECLAIM2`, &0169 `READKEY`, &018A `GETTOKEN`. + +#### NRWRITE +One-instruction write-and-return, used via computed calls when a single byte +must be written while a particular page is switched in. + +#### MODECMD +Implements `MODE n`. Uses `SYNTAX6` to demand a numeric argument, `LIMDB` to +range-check 1–4 (decrementing to 0–3), then dispatches to `MODPT2` in ROM1 +which reprograms the video mode, windows and expansion tables. + +#### S16OP +Output routine for the `$` channel (stream 16 / RECORD TO): preserves AF' and +calls `S16OSR` (misc2.asm) with ROM1 off to append the character to the string +variable named in `STRM16NM`. + +#### R1OFFCL +Family of helpers to execute ROM0-resident code that needs ROM1 paged out +(e.g. to see the BASIC program): `R1OFFCL` calls the address in the inline +word; `R1OFFJP` does the same but discards the return address (a "jump" that +still restores paging); `R1OFFCLBC` calls BC. `R1ONCLBC` is the complement +that forces ROM1 *on*. All preserve every register except AF'. + +#### R1XJP +Pages ROM1 out and jumps to BC without any restore — used when control is +permanently transferring back into ROM0 (e.g. at the end of AUTOLIST). + +#### SOP2 +See PRINTSTR above. + +#### JSVIN +Vector &0103. For external code: switches the system page into section A +(&4000) with ROM0 on/ROM1 off, moves the stack to a safe internal area, calls +the routine whose address follows the call, then restores stack and paging. + +--- + +## vars.asm + +No code — the complete system-variable map, fixed buffers, token codes, DOS +hooks and port numbers. Fully catalogued in +[constants.md](constants.md#varsasm). + +--- + +## editor.asm + +The interactive line editor. It runs as a loop fed by `WAITKEY`, inserting +printable characters at the cursor (`KCUR`) in the edit line (ELINE) or INPUT +workspace, and dispatching editing keys 7–&0F through a pointer table. + +| Entry point | Description | +|---|---| +| [EDITOR](#editor) | Main editor entry: hooks `EDITV`, saves ERRSP, applies AUTO line numbering, then loops on `EDFK`/key dispatch until ENTER | +| [ADDCHAR](#addchar) | Channel "R" output routine: insert a character at KCUR, expanding token bytes ≥&85 to their keyword text (used when a line is listed into the edit buffer) | +| `ADCH1` | Raw insertion of one byte at KCUR (opens 1 byte with MKRMCH) | +| [EDFK](#edfk) | Get a key; if it is a user-defined key (192–254), insert its definition text (auto-ENTER unless it ends in ":") | +| `EKPT` | Editing-key pointer table: EDIT, left, right, down, up, delete-left, ENTER, delete-right, keypad toggle | +| [EDKY](#edky) | EDIT key: list line EPPC into ELINE through channel "R" with pretty-listing off | +| [EDLT / EDRT](#edlt) | Cursor left/right, treating an &FF+code function token as one character | +| `EDDN`/`EDUP` | Cursor down/up: within a line uses the special `CUOP` output to find the matching KCUR; on an empty ELINE moves the `>` program cursor instead (`FUPDN`) | +| `EDKPX` | Toggle the numeric-keypad flag (KPFLG) | +| [EDDLL / EDDLR / EDDLC](#eddll) | Delete left/right; deletes both bytes of FF-prefixed tokens and pulls colour-control parameters correctly | +| `CARET` | Delete one byte at (HL) — also used by LOCAL | +| [EDENT](#edent) | ENTER: unwind to the editor's caller; `ERRCHK`/`RESESP` re-raise any pending error | +| `RESTOP`/`POCHNG`/`DETOHL`/`PRERESTOP` | Channel output-address patching used for control-code parameter collection | +| `CUOP` | Special output routine (via ROM1 `CUOPP`) that watches for the screen position matching the cursor during EDUP/EDDN | +| [WARNBZ / RSPNS / NOISE / BEEPER](#warnbz) | Editor error buzz (RASP) and key-click (PIP) | +| [CLEARSP](#clearsp) | Reclaim the edit line (or INPUT workspace region); `SETKC`/`SETKC2` reset KCUR/KCURP | +| `SETDE` | DE := start of ELINE (edit mode, Z) or of the INPUT line in workspace (NZ) | +| [LNNM](#lnnm) | Read a line number at (HL), falling back to the previous line at program end (DE=0 if no program) | +| `GTKBK`/`WKBR` | Flush keyboard and wait for a key with BREAK checking | +| [WAITKEY](#waitkey) | Wait for a key via the current channel's input routine; "End of file" error if input is exhausted | +| [KYIP / KYIP2](#kyip) | The keyboard channel input routine: returns queued key, handles CAPS lock, and the two-key colour-control sequences (`KYPM`) | +| `EDPRT`/`FONOP` | Print the edit/INPUT line to the lower screen (ROM1 `EDPTR2`); force normal output | +| [AULN](#auln) | AUTO mode: if the edit line is empty, print EPPC+step into it as the new line number | +| [FNDKYD / DKTR](#fndkyd) | Find a DEF KEY definition for key code A (or the list terminator) in the DKDEF buffer | +| `KSCHK` | Z if the current channel letter is K or S | + +### Details + +#### EDITOR +Entered with the edit line (or INPUT buffer) already set up. Pushes an error +frame (`EDER`) so that syntax errors during editing buzz and re-enter the +editor rather than aborting, records the screen position, applies `AULN`, and +then loops: `EDFK` fetches a key, `NOISE` clicks, and the key is either +inserted (`ADCH1`), treated as a two-byte colour control (`TWOKYS`), or +dispatched through `EKPT` to an editing routine. The loop address `EDLP` is +kept on the stack so editing routines simply RET to continue. + +#### ADDCHAR +The output side of channel "R" (the editor channel used by EDIT and AUTO): +characters "printed" to it are inserted into the line at KCUR. Bytes ≥&85 are +expanded back to keyword text via ROM1 `PRGR802` unless the in-quotes flag is +set, so listing a tokenized line into the buffer yields editable text. It also +maintains FLAGS bit 0 (leading-space suppression) so that re-tokenizing the +edited line produces identical spacing. + +#### EDFK +Fetches a key with `WAITKEY`. Codes 192–201 are the function keys/keypad +(returned as digits if KPFLG says numeric). Other codes ≥192 are looked up +with `FNDKYD`; if defined, the definition text is block-inserted at KCUR and, +unless the definition ends with ":", ENTER is simulated (`EDENT`). + +#### EDKY +The EDIT key. In INPUT mode it just clears the input line. Otherwise it takes +the line number from the edit line (or EPPC), finds the line, and lists it +into ELINE by printing it through channel "R" with `OUTLINE` — i.e. the line +is *detokenized* into the edit buffer. KCUR is left just after the 5-digit +line number. + +#### EDLT +Cursor movement treats the two-byte &FF-prefix function tokens as a unit: +moving left over the code byte of an FF-pair steps back an extra byte; EDRT +skips forward over an &FF before landing. Movement stops at the line start +(the byte before is the SAVARS &FF terminator) and at the CR at line end. + +#### EDDLL +Delete-left calls EDLT then deletes at the new position; delete-right deletes +at KCUR unless it is the CR. `EDDLC` deletes 2 bytes for an FF-prefixed token, +and when the byte before the deletion point is a colour control code &10–&15 +it deletes the control byte first and re-points KCUR so the orphaned parameter +is deleted by the next keystroke. + +#### EDENT +ENTER. Discards the editor-loop and warn-buzz frames from the stack and +returns to the editor's caller (the main loop), after `RESESP` restores ERRSP +and re-raises any recorded error. + +#### WARNBZ +On editor-detected errors with the lower screen in use: zeroes ERRNR, emits +the RASP buzz and re-presents the line, rather than aborting to a report. + +#### CLEARSP +Reclaims everything between ELINE and WORKSP−1 (edit mode) or WORKSP and +WKEND−1 (INPUT mode), then resets KCUR (and KCURP from the current paging). + +#### LNNM +Given HL at a line-number MSB, returns DE = that line number; if HL sits on +the &FF program terminator it returns the previous line's number (DE via the +passed-in DE pointer), or 0 for an empty program. + +#### WAITKEY +Sets TVFLAG bit 3 ("edit line needs printing to lower screen") on first call, +then repeatedly calls the current channel's input routine until it returns a +key (CY) — a Z,NC return loops, NZ,NC raises "End of file". + +#### KYIP +The standard keyboard input routine installed in channels K and S. Returns +the queued key from LASTK with CY. CHR$ 6 toggles caps lock; codes &10–&15 +(PEN…OVER controls) switch the channel input to `KYPM`, which validates and +returns the following parameter digit, restoring normal input afterwards. + +#### AULN +If AUTO is on and the edit line is empty, computes EPPC+AUTOSTEP and prints +it into the edit line through channel "R" (refusing numbers ≥ &FF00). + +#### FNDKYD +Scans the DEF KEY buffer at (DKDEF): each definition is `code, len16, text…`, +terminated by an &FF code byte. Returns HL=text, BC=len, CY if absent. `DKTR` +finds the terminator (used to compute free space). + +--- + +## list.asm + +Listing and screen-clear commands, plus the PRINT command itself and the +logic that moves the `>` program cursor up/down the listing. + +| Entry point | Description | +|---|---| +| [AUTOLIST](#autolist) | List the program around EPPC in the upper screen, adjusting SDTOP so the current line is visible without scrolling | +| [LIST / LLIST](#list) | The LIST command: optional FORMAT n, optional #stream, line range via the bracketless slicer; sets EPPC | +| `LIST5`/`LSTLNS` | List from line HL onwards with indenting on (calls ROM1 `LSTR1`) | +| `SPACAN` | Cancel pending pretty-listing indent counts | +| [CLS / CLSBL / MCLS](#cls) | CLS command (and BLITZ entry): clear whole screen or window, reset scroll counts, LPT table and graphics origin | +| [CLSLOWER](#clslower) | Clear (and shrink) the lower screen window, resetting channel K | +| `CLSE`/`CLSG` | Fast full-screen clear using stacked PUSHes (~7 T-states/byte) | +| `CLWC`/`CLWC2` | Clear window and refresh the channel's I/O addresses | +| [PRINT / LPRINT](#print) | The PRINT command: separators, TAB/AT items, colour items, expression printing via STR$ | +| `PRSEPR`/`PRTERM`/`PRITEM` | Print separator/terminator classification and single print-item evaluation | +| `RUNCR`/`PRCIFRN` | Print CR (or char C) only when running | +| [FUPDN / LPD / MWDN / MWUP](#fupdn) | Move the `>` cursor a line up/down the on-screen listing, scrolling the window when leaving it | +| `IOUTLN`/`IOUTLNC`/`IOPCL`/`IOPOF` | Print one program line with indented-output mode switched on/off around it | +| [REALN / ADVEPPC / ADVSTOP / ADVAC](#realn) | Snap a line-number system variable to a real line / advance it to the next line | + +### Details + +#### AUTOLIST +Called after every command execution in the edit loop. Ensures EPPC and SDTOP +name real lines, computes a suitable SDTOP at most ~&200 bytes above EPPC's +address (so the current line appears without scrolling), sets LISTSP so a +"scroll?" refusal aborts cleanly, then lists from SDTOP with indenting on. +TVFLAG bit 4 marks the autolist while it runs. + +#### LIST +`LLIST` presets stream 3, `LIST` stream 2. `LIST FORMAT n` (0–2) just stores +LISTFLG (the pretty-listing indent step). Otherwise an optional `#s`, an +optional `,`/`;`, then a bracketless range (`10 TO 200`, `TO`, `50`…) parsed +by `BRKLSSL` into FIRST/LAST. A single number means "from here to the end" +for LIST. FIRST becomes the new EPPC and `LIST5` prints from there. + +#### CLS +`CLS` alone clears the whole screen (also resetting the graphics origin to +0,0 via a small calculator program), `CLS 1` only the window, `CLS #` +resets windows/streams fully (ROM1 `CLSHS`). `CLSBL` is the BLITZ entry with +A holding the parameter. On a full clear the 30-byte LPT (line pointer table) +is zeroed and LNPTR set to &FF (no cursor line on screen). + +#### CLSLOWER +If the lower window has grown beyond its normal 2 lines (INPUT prompts), the +overlap area is cleared as a temporary window with upper-screen colours +first; then the lower window proper is cleared, channel K selected, and +SPOSNL reset. + +#### PRINT +`PRINT`/`LPRINT` select stream 2/3 when running, set the in-quotes flag (so +tokens print as UDGs, not keywords), copy permanent colour vars to temps, and +loop: separators (`;` nothing, `,` CHR$ 6 column tab, `'` CR) alternate with +items. Items are TAB n, AT r,c, `#s` redirection, colour items, or an +expression: numeric results go through `JPFSTRS` (STR$ to buffer) and strings +print directly from their pages. + +#### FUPDN +Handles cursor-up (&0B)/down (&09) over the listing when the edit line is +empty. Uses the LPT table (one byte per screen row, &FF where a program line +starts) to find the previous/next row bearing a line number; if the cursor +would leave the window, the window is scrolled (`MWDN` measures the incoming +line's height with a dummy "dumped" print first; `MWUP` advances SDTOP and +reprints the boundary lines) and EPPC follows. + +#### REALN +`REALN` replaces the line number in a system variable with the first real +line ≥ it. `ADVEPPC`/`ADVSTOP` advance EPPC/SDTOP one line (used by +cursor-down and scrolling), all through `ADVAC` which combines `FNDLINE` and +`LNNM`. + +--- + +## roll.asm + +ROLL and SCROLL (wrap-around vs. blanking) of arbitrary screen areas, plus the +window-clear and editor-scroll primitives built on the same machinery. The +inner loops execute a run of `LDI`/`RLD`-style opcodes generated into the RAM +code buffer `CDBUFF` (built by `CRTBF`/`CRTBFI` in using.asm). + +| Entry point | Description | +|---|---| +| [ROLL / SCROLL](#roll) | The commands: `dir[,pixels[,x,y,w,len]]`; SCROLL CLEAR/RESTORE toggle the "scroll?" prompt | +| `JROLL` | Jump-table entry with registers pre-loaded (B=pix, C=dir, HL=coords, D=len, E=width, A=roll/scroll) | +| `RLBYTE`/`RUPDN` | Byte-granular left/right movement; up/down movement (also the editor's engine) | +| [RSSTBLK](#rsstblk) | Save the strip that will wrap into RSBUFF (shared with GRAB); errors if > ~8K | +| [CLSWIND](#clswind) | Clear the current character window (scroll up by its own height) | +| [EDRS / EDRS1UP / EDRSADN](#edrs) | Editor scroll of the window by A rows up/down, scrolling the LPT line table alongside | +| `RSMOVSR` | Copy B' scans between screen rows via CDBUFF (main block mover) | +| [CALCPIX / CALCPIXD](#calcpix) | Convert character rows to scan lines using CSIZE (plus double-height adjust) | +| [NEXTUP / NXTDOWN / NEXTDOWN](#nextup) | Step a screen address one scan up/down in modes 0/1 (mode 0's thirds layout handled) | +| `CTAA` | Convert a mode 0 pattern address to its attribute address | + +### Details + +#### ROLL +Parses direction (1=left, 2=up, 3=right, 4=down), optional pixel count, and +an optional x,y,w,len area (widths forced even, coordinates forced to fat). +Requires MODE 2/3 for arbitrary areas. Single-pixel horizontal movement uses +`RLD`/`RRD` runs; multi-pixel horizontal movement moves whole bytes with a +generated LDIR/LDDR run, wrapping the displaced bytes through RSBUFF (ROLL) +or blanking with the paper byte (SCROLL). Vertical movement copies scans with +`RSMOVSR` then wraps or blanks the freed strip. + +#### RSSTBLK +Copies the `pix` scans that are about to be pushed off the area into RSBUFF +(&E003 in the second screen page), recording the byte count in TEMPW2. "Stored +area too big" if the strip exceeds ~8K−19 bytes. GRAB uses the same routine to +capture its block. + +#### CLSWIND +Expresses "clear window" as "scroll up by the whole window height": the same +code path blanks every scan, including the leftover scans between upper and +lower screen when clearing the upper window. + +#### EDRS +The editor/lister scroll. Computes the window geometry (width in bytes per +mode — 1, 1, 1.5/2 or 4 bytes per column), then uses `RUPDN2`. Mode 1 scrolls +pattern data then attributes; mode 0 has its own scan-stepping loop +(`EDRSM0`) using NEXTUP/NEXTDOWN and scrolls the attribute block separately. +`EDRS1UP` also scrolls the LPT table (`STENTS`) so line-number tracking +follows the text. + +#### CALCPIX +\(\text{scans} = \text{rows} \times \text{height}\) where the height comes +from CSIZE (6–32), plus DHADJ (8) when the bottom half of a double-height +character is being placed. + +#### NEXTUP +Address steppers for the interleaved mode 0 layout (character cell thirds) +and linear 32-byte rows of mode 1. Used by scroll, SCREEN$ and the mode 0 +plot routines. + +--- + +## mainlp.asm + +The heart of the interpreter: the syntax-check pass over a line, the +statement dispatch loop used both for checking and running, the main +edit-execute loop, error handling, and line insertion. + +| Entry point | Description | +|---|---| +| [LINESCAN](#linescan) | Syntax-check the edit line: FLAGS bit 7 cleared ("checking"), statements run in check mode, 5-byte forms get inserted | +| [LINERUN / LOOPEL](#linerun) | Run (or continue at statement C in) the edit line; PPC=&FFFF marks "in ELINE" | +| [SEARCH](#search) | DO/DEF PROC helper: find token E (with D intervening) from CHAD or die with the error byte after the call | +| [STMTLP…NEXTSTAT](#stmtlp) | The statement loop: fetch command byte, dispatch via CMDADDRT (ROM1 paged as needed), return to NEXTSTAT | +| [NEXTSTAT / STMTNEXT / LINEEND / LINEUSE / NEXTLINE](#nextstat) | Between-statement logic: BREAK check, pending jumps (NSPPC/NEWPPC), line advance, statement skipping | +| `REMARK` | REM: discard the rest of the line | +| `BRKCR`/`BRKSTOP`/`BRKTST` | BREAK (ESC) tests: error 14 ("BREAK - CONTINUE to repeat"), error 15 ("BREAK into program") | +| [MAINEXEC / MAINELP](#mainexec) | The main edit loop: AUTOLIST, EDITOR, TOKMAIN, LINESCAN, then insert the line or run it | +| [MAINER](#mainer) | Post-execution/error return: ON ERROR dispatch, CONTINUE bookkeeping, report printing | +| [ERRHAND1 / ERRHAND2](#errhand1) | Print the error report (with variable name for error 2), set OLDPPC/OSPPC for CONTINUE | +| `DFKNL` | DEF KEYCODE tail: syntax-check the rest of the line then strip its 5-byte forms | +| [REMOVEFP](#removefp) | Remove all invisible &0E+5-byte forms from (HL) to the CR | +| [EVALLINO](#evallino) | Parse the line number at the start of ELINE into BC (Z if none, CY if >65279) | +| `AELP`/`STPGS` | Address ELINE / set CLAPG=CHADP=NXTLINEP from A | +| [INSERTLN](#insertln) | Insert the tokenized, checked edit line into the program (replacing any existing line) | +| [SKIPSTATS / SKIPCSTAT / SKIPS0](#skipstats) | Skip D statements (or the current one) respecting quotes and number forms | +| `DATA`/`DATA1` | DATA: skip the statement when running; syntax-check items when checking | + +### Details + +#### LINESCAN +Resets FLAGS bit 7 (syntax-check mode), IFTYPE, SUBPPC and ERRNR, evaluates +the line number, then falls into the statement loop. In check mode every +command routine validates its arguments — and the expression evaluator's +literal handler *writes the 5-byte forms into the line* (see +[tokenized-program-format.md](tokenized-program-format.md)). `ABORTER` makes +command routines return early instead of executing. + +#### LINERUN +Runs the edit line: CLA's page byte is zeroed (an ELINE marker that RETURN +and NEXT recognise), PPC=&FFFF (prints as line 0), CHAD points at the line +and NXTLINE at its end; then the shared NEXTLINE path starts statement 1. + +#### SEARCH +Wrapper over `SEARCHALL` (tadjm.asm) used by DO (find LOOP), DEF PROC (find +END PROC) and long IF. On failure it jumps to &0008 so the byte following the +CALL becomes the error code; on success it discards its return address and +resumes execution at the found position (`EXCHAD2` re-establishes PPC and +NXTLINE from CLA, and skips a WHILE/UNTIL condition after LOOP). + +#### STMTLP +For each statement: skip spaces, handle ":" and CR, store CSA, offer the +command byte to the `CMDV` hook, then require a byte ≥ &90 (below &90 with a +letter means a PROC call — `PROCS`). The byte indexes CMDADDRT (a word table +in ROM1, base held in the CMDADDRT sysvar); bit 15 of the entry selects +whether the routine lives in ROM1 (leave it paged) or ROM0 (page it out). +The command routine is jumped to with `NEXTSTAT` pushed as its return. + +#### NEXTSTAT +After each statement: BREAK check, page CHADP back in. If NSPPC ≠ &FF a jump +is pending: NEWPPC=&FFxx means the edit line, otherwise `FNDLNHL` locates the +line (GOTO tolerates a missing line by landing on the next one; RETURN etc. +insist via `STATLOST`). `LINEUSE`/`NEXTLINE` update CLA/PPC/NXTLINE and +either enter the first statement or `SKIPSTATS` to the requested one. + +#### MAINEXEC +The outer loop of the whole machine: AUTOLIST; `SETMIN` (empty edit line); +EDITOR (returns on ENTER); **TOKMAIN** (tokenize the typed line); **LINESCAN** +(syntax-check, embedding number forms); then if the line starts with a number +→ `MAINEADD`/`INSERTLN`, else set FLAGS bit 7 ("running"), NSPPC=1, run +COMPILE (labels/FN/PROC address caching) and LINERUN it. + +#### MAINER +Where every run ends (normally or via RST 8, since ERRSP points here). Fixes +SUBPPC after ON, cancels DEFADD/XPTR/AUTO, restores display 0 and the edit +line. If ON ERROR's temporary flag was armed and the error isn't OK/STOP, +control transfers to the statement after the recorded `ON ERROR` instead of +printing a report. Otherwise `ERRHAND1` prints `nn message, line:stat` (error +&50 prints the MGT banner; DOS errors ≥&51 fetch messages from the DOS page) +and `ERRHAND2` records CONTINUE information. + +#### ERRHAND1 +See MAINER. Error 2 ("… not found") prints the offending variable name from +TLBYTE/NMBUFF, appending `$` or `()` by type. + +#### REMOVEFP +Walks a statement byte-by-byte; wherever it finds &0E it reclaims 6 bytes. +Used before re-editing text that was syntax-checked (INPUT lines, DEF +KEYCODE) so the invisible forms never reach the user. + +#### EVALLINO +Addresses ELINE, calls `SMBW` (set MEM to a scratch area), converts leading +digits via INTTOFP/FPTOBC. Returns BC=number, Z if zero/absent, CY if >65279 +(&FEFF is the highest legal line). + +#### INSERTLN +Sets COMPFLG (whole program needs recompiling), trims a single leading space +after the line number, limits the text to &3EFF bytes, finds/reclaims any +existing line with `FNORECL`, then opens `len+4` bytes and writes: line +number **MSB first**, then length (LSB first), then copies the text (including +its embedded 5-byte forms and calling buffers) from ELINE with FARLDIR. A +bare line number (text = just CR) deletes the line. + +#### SKIPSTATS +The statement skipper: counts ":" and THEN as statement boundaries, ignores +them inside quotes, skips &0E forms via `NUMBER`, stops at CR. Exits with +CHAD on the boundary character. Used by ON, error recovery, DATA, statement +addressing after jumps. + +--- + +## misc1.asm + +| Entry point | Description | +|---|---| +| [PRHSH1 / PRHSH2](#prhsh1) | Parse `#stream` in LIST/PRINT and select it | +| [STRMINFO / SETSTRM](#strminfo) | Map stream number (−5…16; 16→−4) to its STREAMS entry; select channel, set DEVICE and CLET | +| `STREAMFE`/`STREAMFD` | Select stream −2 ("S", main screen) / −3 ("K", lower screen) | +| `CHANFLAG` | Make (CURCHL) current: record channel letter, DEVICE for K/S/P, fall into TEMPS | +| [TEMPS / GTEMPS / GRATEMPS](#temps) | Copy permanent colour/graphics variables to the temporary set; select window; build the colour expansion table | +| `COLEX` | (Re)colour the mode 2/3 expansion table from M23PAPT/M23INKT | +| [POKE / DPOKE / PDPSUBR](#poke) | POKE n,list / POKE n,a$ / DPOKE; the shared 0–512K address-to-paging resolver | +| `CHKMD23` | Error 34 unless MODE ≥ 2 | +| [READ](#read) | READ [LINE] var…: find next DATA item (searching from CLA when exhausted), assign via VALFET1 or as a raw LINE string | +| `SYNT9SR`/`CITEM`/`CITEMSR` | Colour-item parsing for PLOT/CIRCLE/FILL/PRINT (`INK n;` etc.) | +| [PERMS](#perms) | The INK/PAPER/FLASH/BRIGHT/INVERSE/OVER commands: run the item then copy temps to permanents | +| `LDIR8` | Copy the 8 temp colour bytes to the permanent set | + +### Details + +#### PRHSH1 +`PRHSH1` (from LIST) checks for `#`; `PRHSH2` (from PRINT) evaluates the +stream number and selects it via `STRMINF2`+`STSM2` (error 47 if unopen). + +#### STRMINFO +Streams live at &5C0C–&5C35 as 16-bit displacements into the CHANS area +(0 = closed). Stream 16 is transformed to internal stream −4 (output into a +string — RECORD). `SETSTRM` stores STRNO, resolves the pointer and falls into +`CHANFLAG`, which also sets DEVICE (S=0, K=1, P=2) and runs TEMPS for K/S/P. + +#### TEMPS +Copies THFATP…GOVERP (9 bytes) to the temporary set; for the lower screen +forces OVER/INVERSE 0 and BORDCR/M23LSC colours. Selects the window +rectangle for the device. `COLEX` then builds CEXTAB: the 16 (mode 2) or 32 +(mode 3) expansion-table bytes masked into ink/paper colour bytes so the +per-mode print routines can emit coloured pixels straight from a table. +`GRATEMPS` is the graphics variant (always upper screen, skips COLEX). + +#### POKE +POKE accepts an address then either a string (block-copied with FARLDIR) or up +to 32 numbers (stored ascending from the address). Addresses are 0–&1FFFF +relative to the context base page: `PDPSUBR` unstacks the address, and for +0–&FFFF maps 0–&3FFF→ROM0, &4000–&7FFF→base page, &8000–&FFFF→base+1/2, while +≥&10000 pages the target into &8000–&BFFF directly. DPOKE/PEEK/DPEEK/CALL/USR +share it. + +#### READ +For each variable: if DATADD points mid-list (space or comma) continue there, +else `SRCHPROG` for the next DATA statement (error 3 when none). Normal READ +assigns with `VALFET1` (full expression evaluation against the DATA text); +READ LINE copies the raw text (quotes stripped of FP forms via a workspace +copy and `REMOVEFP`) and assigns it as a string. CHAD is preserved in the +auto-adjusting PRPTR while the data pointer is walked. + +#### PERMS +Executes the colour item for the current command token (via `COTEMP4` → +`PRCOITEM` in ROM1, which edits the temporary variables), then copies +ATTRT…GOVERT over ATTRP…GOVERP, making the change permanent. + +--- + +## lookvar.asm + +Variable lookup. Also the definitive record of how variables are stored — see +the detail sections. + +| Entry point | Description | +|---|---| +| [LOOKVARS / LKVARS2](#lookvars) | Parse the name at CHAD, copy it to NMBUFF, and search the numeric chains or the string/array area; NZ=found with HL at the value | +| [NUMLOOK](#numlook) | Search the per-letter linked lists of numeric variables for the name in FIRLET | +| `LKBSV` | Look up a name stored on the BASIC stack (PROC teardown helper) | +| [STARYLK / STARYLK2](#starylk) | Search the string/array area for the name in FIRLET (type/length byte in C) | +| [NAMTOBUF](#namtobuf) | Copy a variable name to FIRLET (lower-cased, spaces stripped), classifying it ($, (, array bits) | +| `LVFLAGS` | LOOKVARS then return FLAGS×2: M=numeric, P=string, CY=running, Z' = not found | + +### Details + +#### LOOKVARS +Front end: `NAMTOBUF` scans the name, sets FLAGS bit 6 (numeric/string) and C +(bits 4–0 name length−1; bit 6 string array; bit 5 numeric array), and CHAD +is moved past the name and any `$`/`(`. Simple numerics go to NUMLOOK (in run +time), everything else to STARYLK. On exit: NZ=found, HL→value (numbers: 5 +bytes; strings: length-in-pages byte), C=type/length byte from the variables +area; Z=not found with HL at the chain terminator (numbers) or the SAVARS +&FF stopper (strings). + +#### NUMLOOK +Numeric variables are stored as 26 per-letter chains. NVARS points at 26 +16-bit *relative* pointers (one per letter a–z, MSB &FF = empty). Each +variable record: + +| Field | Size | Contents | +|---|---|---| +| Type/len byte | 1 | See bit table below | +| Pointer | 2 | Relative offset from its own location to the next variable of this letter (&FFFF ends the chain) | +| Name tail | 0–31 | 2nd…nth name letters (first letter implied by the chain) | +| Value | 5 | The number (integer or FP form) | +| FOR extension | 19 | Only if bit 6 set: limit (5), step (5), looping page (1), address (2), statement (1) | + +Type/len byte bits: + +| Bit | Meaning | +|---|---| +| 7 | Hidden (PROC-local shadowing) | +| 6 | FOR-NEXT variable (record extended as above) | +| 5 | Unused slot (re-usable by PROC locals) | +| 4–0 | Name length − 1 (0–31) | + +The search compares the type/length byte (ignoring the FOR bit) then the +name tail; page overflow is handled so chains can span 16K boundaries. + +#### STARYLK +Strings and arrays live in a separate area (SAVARS→ELINE), terminated by an +&FF byte. Each record: + +| Field | Size | Contents | +|---|---|---| +| Type/len byte | 1 | Bit 7 = hidden; bit 6 = string array; bit 5 = numeric array (bits 6 and 5 both clear = simple string); bits 4–0 = true name length (max 10) | +| Name | 10 | Name, padded to 10 characters | +| Length (pages) | 1 | Data length ÷ 16K | +| Length (mod 16K) | 2 | Data length remainder | +| Data | … | String text; or dimension count, dimension sizes (words) and elements for arrays (5 bytes per numeric element; fixed-width rows for string arrays — see DIM in assign.asm) | + +Search compares type (ignoring bit 6 mismatch so `a$` finds a 1-D string +array) and name; STRLOCN tracks the current record. + +#### NAMTOBUF +Names may contain letters, digits, `_` and embedded spaces (removed); up to +32 characters for numerics, but string/array names are limited to 10 (error +40 otherwise, at STARYLK). The first character is stored lower-cased at +FIRLET; matching elsewhere is case-insensitive (`AND &DF`). + +--- + +## eval.asm + +The expression evaluator — an operator-precedence scanner driven from +`SCANSR`, with function/operator priorities in tables, plus the routines that +convert literal text numbers to 5-byte forms. + +| Entry point | Description | +|---|---| +| [SCANNING / SCANSR](#scanning) | Evaluate the expression at CHAD. Running: result on the FP stack. Checking: syntax verified and 5-byte forms inserted. FLAGS bit 6 = result type | +| `SLETTER` | Variable reference (checks DEF FN parameter buffers first via LKFNVAR) | +| [ABOVLETS / IMMEDCODES](#abovlets) | Handle an &FF-prefixed function token: immediate functions dispatch through IMFNATAB, FPC functions get queued with priority &CF | +| [SDECIMAL / INSERT5B / LK0ELP](#sdecimal) | Literal number: at check time compute and insert `0E xx xx xx xx xx`; at run time skip the digits and copy the 5 bytes to the FP stack | +| [OPERATOR / SLOOP / PRIGRTR](#operator) | Binary-operator recognition (+−*/^=<>… and FF-prefixed MOD…>=), priority comparison and deferred execution | +| `OPPRIORT` / `FNPRIORT` | Priority tables for binary operators and odd-priority unary functions (bit 7 = numeric result, bit 6 = numeric argument) | +| `IMFNATAB` | Address table for "immediate" functions (PI, RND, POINT, FN, BIN, INSTR, INKEY$, SCREEN$, MEM$ …) | +| [SQUOTE / SQUOTE2](#squote) | String literals: simple strings are stacked in place; embedded `""` pairs force a copy to a buffer/workspace | +| [CALC5BY](#calc5by) | Convert decimal / `&hex` / BIN binary literal text to a value on the FP stack | +| [INTTOFP](#inttofp) | Accumulate ASCII digits at CHAD into a number on the FP stack | +| `R0USR`/`R0USRS`/`CALLX` | USR/USR$ and the CALL trampoline (paged call to machine code) | +| `IMMEMRYS`/`IMHIMEM`/`IMMEM`/`IMMOUSEX`… | Immediate functions: MEM$(a TO b), RAMTOP, FREE, XMOUSE/YMOUSE, XPEN/YPEN, IN, PI, ITEM | +| [FPSWOP13 / FPSWOP23 / SWOP12 / FPSWOP](#fpswop13) | FP-stack entry swaps (also FPC operations &06/&1C/&1D) | +| `STKPGFORM`/`STK19BIT` | Stack a page-form (page,addr) or 19-bit address as a number | + +### Details + +#### SCANNING +`SCANNING` wraps `SCANSR` in an ROM1-off call and returns C=current char, +A=FLAGS. `SCANSR` pushes a priority-0 stopper, then loops: each token is +classified (letter → variable; FF → function/alphabetic operator; digit/./& → +literal; quote → string; bracket, unary ± …). Functions and operators are +pushed as (priority, code) pairs; whenever the incoming priority does not +exceed the top of the stack, the stacked operation is executed (running) or +type-checked (checking) via the calculator's `USEB`. String operands remap +operator codes (+7 for comparisons, CONCAT for `+`), enforcing type rules at +check time (bit 6 of the priority byte vs FLAGS bit 6). The scan ends when +both priorities are 0; exit is through RST &18 so A=terminating char. + +#### ABOVLETS +On &FF: the next byte −&1A gives the internal code (&21–&69). Codes below SIN +are "immediate" — evaluated at once because they take no argument or +bracketed arguments (dispatch through IMFNATAB, with NUMCONT/STRCONT setting +the result type). SIN…EOF-range codes and NOT/NEGATE/VAL-class codes are +queued like operators with their priority from FNPRIORT (default &CF: +priority 15, numeric in/out). + +#### SDECIMAL +The heart of literal optimization. **Check time** (`INSERT5B`): CALC5BY +computes the value; `MAKESIX` opens 6 bytes at CHAD — i.e. *after the ASCII +digits* — writes &0E, and the value is popped off the FP stack into the 5 +bytes. **Run time** (`LK0ELP`): the digits are skipped by scanning forward to +the next &0E and the following 5 bytes are LDIR'd straight onto the FP stack +— no text conversion ever happens while running. + +#### OPERATOR +Recognises single-character operators by code arithmetic (`SUB "*"+1` maps +the ASCII character to an internal operation code; `^` is special-cased) and +the FF-prefixed alphabetic operators and two-character comparisons (stored +token codes &7A–&83 map to internal codes &08–&11). The internal code +indexes OPPRIORT for the priority byte, whose low nibble is the binding +priority: + +| Operator | Stored form in the line | Internal code | Priority | +|---|---|---|---| +| `*` | ASCII &2A | &00 | 8 | +| `+` | ASCII &2B | &01 | 6 | +| `-` (binary) | ASCII &2D | &03 | 6 | +| `^` | ASCII &5E | &04 | 15 | +| `/` | ASCII &2F | &05 | 8 | +| MOD | `FF 7A` | &08 | 14 | +| DIV | `FF 7B` | &09 | 14 | +| BOR | `FF 7C` | &0A | 2 | +| BXOR | (`FF 7D` — unused slot) | &0B | 2 | +| BAND | `FF 7E` | &0C | 3 | +| OR | `FF 7F` | &0D | 2 | +| AND | `FF 80` | &0E | 3 | +| `<>` | `FF 81` | &0F | 5 | +| `<=` | `FF 82` | &10 | 5 | +| `>=` | `FF 83` | &11 | 5 | +| `<` | ASCII &3C | &12 | 5 | +| `=` | ASCII &3D | &13 | 5 | +| `>` | ASCII &3E | &14 | 5 | +| NOT (unary) | `FF 76` | — | 4 | +| Unary minus (NEGATE) | ASCII &2D | — | 9 | + +(Unary functions default to priority 15 via FNPRIORT.) Bits 7/6 of each +priority byte declare the result/argument types for the check-time type +rules. When the left operand is a string, `+` is remapped to CONCAT (code +&02) and each comparison/AND to its string variant (numeric code + 7, +giving calculator ops &15–&1B); `$ MOD $` and the like are rejected as +nonsense. + +#### SQUOTE +A quoted string with no doubled quotes is *not copied anywhere*: its start +address/page/length in the BASIC line itself are stacked (STKSTOREP). Only +strings containing `""` escapes are copied (de-escaped) into INSTBUF and then +workspace. This is the ROM's "string literal optimization": literals live in +the program and are referenced in place. + +#### CALC5BY +Dispatch on first char: `&` → ROM1 `AMPERSAND` (hex, up to 6 digits); +BIN token (&FF &43 already tokenized — the &43 code is seen here) → binary +digits accumulated into BC (error 28 past 16 bits); otherwise DECIMAL: +integer part via INTTOFP, optional fraction accumulated with a ×0.1 +multiplier loop, optional `E±nn` applied via ROM1 `POFTEN`. + +#### INTTOFP +Total = total×10 + digit, on the FP stack, using calculator code with the +digit in BREG. Stops at the first non-digit (fetched with NXCHAR so spaces +terminate a number). + +#### FPSWOP13 +Register-level swap of FP-stack entries used by FOR (ordering value/limit/ +step) and OPEN; the same code implements calculator ops SWOP, SWOP13, SWOP23. + +--- + +## do.asm + +Control flow: DO/LOOP, IF in both forms, FOR/NEXT, GOTO/GOSUB/RETURN +support, the BASIC stack, and the line finder. + +| Entry point | Description | +|---|---| +| [DO / LOOP / LOOPIF / EXITIF](#do) | DO [WHILE/UNTIL cond] … LOOP [WHILE/UNTIL cond]; LOOP IF / EXIT IF | +| [BSTKE](#bstke) | Push a return frame (type/page, line addr, statement) on the BASIC stack; types &80=DO, &40=PROC, 0=GOSUB | +| [RLEPCOM / RLEPC2](#rlepcom) | "Goto statement C in line at page A, addr HL" — shared resume path for RETURN/END PROC/LOOP/NEXT | +| [ON](#on) | ON n: skip to the n'th following statement; PROC/GOSUB targets fake statement 0 of the next line so they return correctly | +| `GOTO2`/`GOTO3`/`GOTO4` | Set NEWPPC/NSPPC for a jump (range check 0–65279) | +| `CONTINUE` | Jump to OLDPPC/OSPPC | +| [CALBAS](#calbas) | Call a BASIC line from machine code; errors return here with A=code | +| `RETURN` / [ENDPROC](#endproc) | Pop a GOSUB/PROC frame and resume at the next statement (END PROC also tears down LOCALs and re-arms ON ERROR) | +| `WHUNT` | Evaluate an optional WHILE/UNTIL condition into a loop/no-loop carry flag | +| [RETLOOP / RETLOOP2](#retloop) | Pop a BASIC-stack frame of the required type (NZ if wrong type/empty) | +| [FNDLNHL / FNDLNBC / FNDLINE](#fndlnhl) | Find line number HL/BC: from PROG, or from the current line when running and the target is ahead | +| [LIF / SIF / LELSE / ELSE / ENDIF](#lif) | Long and short IF; note the token rewriting between LIF/SIF and LELSE/ELSE forms | +| `TRUETST` | Drop the top FP value and set NZ if it was non-zero | +| [FOR](#for) | FOR v=a TO b [STEP c]: build the 20-byte FOR record, or skip to the matching NEXT if no iteration possible | +| [NEXT / NEXTSR / NEXTTEST](#next) | NEXT v: integer fast path, FP fall-back, loop-back via the stored line/statement | +| [ONERROR](#onerror) | ON ERROR: record ERRLN/ERRSTAT and arm ONERRFLG (or STOP to disarm) | + +### Details + +#### DO +DO with a false WHILE/UNTIL (or EXIT IF true) searches forward for the +matching LOOP (`SEARCH` with intervening-token DO, so nesting is respected) +and continues after it; otherwise `BSTKE` pushes a DO frame (type &80) and +execution proceeds. LOOP pops the frame and loops back (re-pushing happens on +the next DO execution — the frame stores the *DO line* start and statement). +LOOP IF condition pops-and-loops only when true; EXIT IF pops and skips to +LOOP when true. + +#### BSTKE +The BASIC stack grows *down* from BSTACK (&4AFF) toward HEAPEND; each frame +is 4 bytes: type|page, line-address (word), statement. "BASIC stack full" +(41) when it would meet the heap. The frame's address is CLA (line start) — +so RETURN/LOOP resume by line+statement, not by CHAD. + +#### RLEPCOM +Common resume: if the stored address MSB is 0 the frame refers to the edit +line (resume via LOOPEL / NSPPC); otherwise page in the stored page and enter +`RLEPI` in mainlp.asm with the line start and statement number. + +#### ON +`ON n` adds n to SUBPPC and skips n statements. If the target statement is a +PROC call or GOSUB, CHAD is left at the ":" and CLA is temporarily set to +NXTLINE with SUBPPC=255, so the called routine's return lands at statement 1 +of the *next line* (ONSTORE preserves the real statement for error reports). +GOTO executes normally; any other statement executes with a forced line-end +return so exactly one statement runs. + +#### CALBAS +Public vector &010F. Pushes a GOSUB frame whose statement byte is &FF; when +the called line RETURNs, the &FF statement makes RETURN pop back into +CALBAS's error frame instead of BASIC. Exit Z if OK else A=error number. + +#### ENDPROC +Pops a PROC frame (`DPRA`, error 12 if none), calls `DELOCAL` (nparpro.asm) +to unwind local/REF variables, restores ON ERROR's temporary bit if the +permanent bit is set, and resumes at the frame's statement+1. + +#### RETLOOP +Reads the frame at BSTKEND without popping unless the type bits (&E0 mask) +match B; returns A=type/page, HL=line address, C=statement, and advances +BSTKEND on success. + +#### FNDLNHL +Finds a line by number. When running and the target ≥ PPC, the search starts +at CLA (current line) instead of PROG — a big win for forward GOTOs. Walks +line headers (number MSB-first compare, then length) handling 16K page +crossings. Exit: HL→line-number MSB of the found-or-next line, DE→previous +line, Z if exact match. + +#### LIF +The tokenizer always produces the LIF token (&D7) for "IF" (it is first in +the keyword list). At *syntax-check* time the IF routine inspects its own +statement: if a THEN follows the condition, the command byte in the line is +**rewritten** to SIF (&D8) — so the stored program distinguishes long IF +(block form, terminated by END IF) from short IF (one-line THEN form). +Similarly ELSE tokenizes as LELSE (&D9) and is rewritten to ELSE (&DA) when +the preceding IF on the line was short; `LELSE LIF cond` becomes +`LELSE SIF cond` (ELSE IF chains). At run time a false long IF searches for +LELSE/END IF with nested LIFs counted (`SRCHALL3`); a false short IF searches +the line for ELSE. + +#### FOR +Assesses the control variable with SYNTAX4 (marks the type/len byte bit 6), +evaluates value/limit/step, then `ASSISR` assigns the value — creating or +reusing a FOR-type record which extends the normal 5-byte value with limit +(5), step (5), looping page (1), looping address (2), looping statement (1). +The looping point is the next statement (or statement 1 of the next line). +If `NEXTTEST` says no iteration is possible, the interpreter searches forward +for `NEXT v` (matching the variable name) and continues after it (error 6 if +absent). + +#### NEXT +Looks up the variable (STRLEN's bit 6 confirms FOR type, else error 5 +NEXT-without-FOR at NWFERR... error 0/5 accordingly). `NEXTSR` attempts pure +16-bit integer add of step to value with overflow detection; on overflow or +FP forms, value/limit/step are copied to calculator memories and +`V=V+S` / `NEXTTEST` run in FP. If the limit test passes, RLEPCOM resumes at +the stored looping line/statement. + +#### ONERROR +`ON ERROR STOP` clears ONERRFLG. Otherwise records the current line/statement +in ERRLN/ERRSTAT and sets ONERRFLG=&81 (temporary+permanent armed) — except +in the edit line, where it is disabled. MAINER consumes the temporary bit on +the next error and re-enters the program at the statement after ON ERROR. + +--- + +## tadjm.asm + +A grab-bag of core services: keyboard fetch, FP-stack access, program search, +memory open/close, page-form (19-bit address) arithmetic. + +| Entry point | Description | +|---|---| +| `NMISTOP` | NMI "super-break": force error 15 with a fresh stack | +| [GETKEY / READKEY / KBFLUSH / KEYRD](#getkey) | Keyboard fetch primitives (READKEY rescans two-key rollover; used by INKEY$) | +| [STACKA / STACKBC / STACKHL / STKSTORE / STKFETCH](#stacka) | Push/pop 5-byte entries on the FP calculator stack (numbers or string descriptors) | +| `STKSTOREP`/`STKST0`/`STKSTOS` | Stack a string descriptor with the current/masked page | +| [FDELETE / HLTOFPCS](#fdelete) | Fast drop of the top FP entry / push 5 bytes from (HL) | +| [GETINT / GETBYTE / FPTOBC / FPTOA](#getint) | Unstack the top value as an integer (error 30 if negative or out of range) | +| [SETMIN / SETWORK / SETSTK](#setmin) | Reset edit line / workspace / FP stack to empty | +| [SRCHPROG / SEARCHALL / FINDERS](#srchprog) | The program searcher: find token E, with token D counted as "intervening" (nesting), optional second target C, one line or whole program | +| [MKRMCH / MKRM1 / MAKEROOM / MKRBIG](#makeroom) | Open 1/BC/ABC bytes at (HL), moving everything above and adjusting the 14 memory pointers | +| `FNORECL`/`NORECL` | Find and delete a program line | +| [RECLAIM1 / RECLAIM2 / RECL2BIG](#reclaim2) | Close up DE→HL / BC bytes / ABC bytes at (HL) | +| [WKROOM](#wkroom) | Extend the workspace end by BC bytes; DE=room start | +| `ASSV`/`AFLPS` | Adjust a stored page/addr triple (FOR records, BASIC-stack frames) after a move | +| [ADDRSV family](#addrsv) | ADDRELN/ADDRPROG/ADDRCHAD/ADDRWK/ADDRNV/ADDRSAV/…: page in and load HL from a (page,addr) system variable | +| `NEXTONE`/`DIFFER` | Get the extent of a program line / BC = HL−DE | +| `LIMBYTE`/`LIMDB` | Range-check the FP top against limit D, raising error E | +| `SPLITBC` | Split BC into PAGCOUNT/MODCOUNT for FARLDIR | +| `GETROOM` | Free memory (RAMTOP − workspace end) as a 19-bit value | +| [PGOVERF / DECPTR / CHKPTR](#pgoverf) | Correct an address that stepped over/under the &8000–&BFFF window, adjusting the page | +| [ADDAHLBC / SUBAHLBC / ADDAHLCDE / SUBAHLCDE / PAGEFORM / AHLNORM](#addahlbc) | Page-form ↔ 19-bit address arithmetic | +| `SETESP` | Push an error-return frame (old ERRSP saved on stack) | +| `RDRLEN`/`RDLLEN`/`RDTHREE` | Read 3-byte lengths from the tape header buffers | +| [EDGE2 / EDGSENS](#edge2) | Tape-edge timing loop (returns pulse length in C; NC on BREAK/timeout) | + +### Details + +#### GETKEY +`KEYRD` calls the ROM1 scanner then reports the buffered key; `READKEY` (the +INKEY$ back-end) does a fresh two-key scan (`TWOKSC`) and translates through +the key map; `KBFLUSH` empties the queue and clears the new-key flag. + +#### STACKA +All values on the calculator stack are 5 bytes. `STKSTORE` writes A,E,D,C,B +in order — for a string that is page/flags, start-lo, start-hi, len-lo, +len-hi; for a small integer 0, sign, lo, hi, 0. `STKFETCH` is the exact +inverse. STKSTOREP variants insert the current URPORT page with bit 7 +signalling "delete old copy after assignment" (set for simple unsliced +strings). + +#### FDELETE +STKEND −= 5 without copying; returns HL pointing at the dropped value so +callers can read or move it (this is how values are copied out). + +#### GETINT +FPTOBC forces the top value to small-integer form (adding 0.5 and INT if it +is in FP form), then returns BC/HL/A with CY if |value| ≥ 65536, NZ if +negative; GETINT/GETBYTE raise error 30 on CY/NZ (GETBYTE also for >255). + +#### SETMIN +Writes CR,&FF at ELINE (empty edit line + program-area terminator), points +WORKSP after it, and falls into SETWORK (WKEND=WORKSP) and SETSTK +(STKEND=FPSBOT). + +#### SRCHPROG +The generalised finder used by DO/LOOP, IF/ELSE, DEF FN/DATA searches, RENUM +and FOR. Inputs: E=target token, D=intervening token (nesting counter; +THENTOK means none), C=secondary target armed when nesting depth returns to +zero (LELSE handling), B'=&FF for whole program or 0 for one line, all +scanned from CHAD. Skips &0E forms, quoted strings and REM statements; +tracks statement numbers in A'. Returns CY=found with CHAD just past the +target and A=statement number. + +#### MAKEROOM +Opens space at (HL): checks room (insisting on a 150-byte reserve except for +`MKRMCH`), then calls ROM1 `XOINTERS` (using.asm) which adjusts the 14 +pointer system variables (SAVARS…PRPTR) and computes the block to shift, and +FARLDDR moves it up. `MKRBIG` takes a page-form length (A pages + BC). +Everything from the insertion point to WKEND moves; auto-adjusted pointers +(including KCUR, CHAD, XPTR, PRPTR when temporarily holding user pointers) +follow automatically. + +#### RECLAIM2 +The inverse: XOINTERS with CY adjusts pointers down and FARLDIR closes the +gap. RECLAIM1 takes start (DE) and end (HL); RECL2BIG a page-form size. + +#### WKROOM +Grows the workspace at WKEND by BC bytes (no shifting — workspace is the top +of the moving region). Returns DE=start of the new room, HL=end. + +#### ADDRSV +Each of these loads A=page byte, pages it in (section C), and returns HL = +the stored address, exploiting the fixed layout `page, addr-lo, addr-hi` of +the pointer system variables. + +#### PGOVERF +When pointer arithmetic overflows &BFFF (or underflows &8000), these fix the +address back into the &8000–&BFFF window and step URPORT accordingly. + +#### ADDAHLBC +The page-form address kit: AHL (A=page 0–31, HL=&8000–&BFFF) ⇄ 19-bit linear +via AHLNORM/PAGEFORM; add/subtract BC or another page-form CDE. All memory +sizing in the ROM uses these. + +#### EDGE2 +The tape input timer: alternates border colour, watches the EAR bit with a +47-T loop counting in C, returns CY=edge found, NC,Z=timeout, NC=BREAK. + +--- + +## graph0.asm + +| Entry point | Description | +|---|---| +| [CIRCLE / JCIRCLE](#circle) | CIRCLE x,y,r — 8-way symmetric point-plotting circle with thin-pixel support | +| [DRAW / JDRAW / JDRAWTO / DRAWLINE](#draw) | DRAW dx,dy / DRAW TO x,y (+ optional curve angle) — Bresenham-style line with off-screen run-off handling | +| `THINDRAW` | The 512-pixel-wide (mode 2 thin) line variant | + +### Details + +#### CIRCLE +After SYNTAX9 (colour items + coords) and radius, plots the four axis points +then walks one octant, mirroring each point 8 ways (`CIRCEX`→`CIRC3/4/5`). +The per-point plot goes through the IY vector chosen by SETIY (or the +thin-pixel `THCIRCSR`). Radius 0 degenerates to PLOT. BLITZ recording code 3. + +#### DRAW +`DRAW TO` converts absolutes to displacements from the current position; +plain DRAW takes signed displacements (through XRG/YRG scaling). An optional +third argument makes a curve (DRCURVE in using.asm approximates it with +chords). The line loop tracks error in C, taking straight or diagonal steps +via pre-computed step routines, plotting through IY; if the line would leave +the screen, `PLOTCHK`/`RUNOFF` clips it and "Off screen" (32) is raised at +the end unless it started off-screen. Updates XCOORD/YCOORD to the endpoint. + +--- + +## graph1.asm + +| Entry point | Description | +|---|---| +| [PLOT / JPLOT / PLOTFD / THINPLOT / TDPLOT](#plot) | PLOT x,y in all modes, fat and thin pixels; records to BLITZ string (code 1) | +| `M0DPLOT`/`M1DPLOT` | Mode 0/1 plot used by DRAW (pattern bit + attribute) | +| `M3DPOV0…M3DPOV3` | Mode 2/3 nibble plots for OVER 0/1/2/3 (force, XOR, OR, AND), each with inverse variants | +| [SETIY](#setiy) | Choose the plot routine for the current mode/OVER/INVERSE into IY; A=ink nibble for modes 2/3; hookable via SETIYV | + +### Details + +#### PLOT +`PLOTFD` fiddles coordinates through XOS/YOS/XRG/YRG (Y ends 0 at top), +updates YCOORD/XCOORD, selects the screen page, and jumps through IY. Thin +plot computes +\(\text{addr} = \&8000 + Y \times 128 + \lfloor X/4 \rfloor\) +and rotates a 2-bit mask into place, honouring OVER and INVERSE against +M23INKT/M23PAPT. + +#### SETIY +Dispatch matrix: mode 0/1 → M0DPLOT/M1DPLOT; modes 2/3 → one of the four +OVER routines, pre-loading D' with the ink nibble (doubled for even/odd +pixels). GOVERT (graphics OVER 0–3) selects XOR/OR/AND variants; INVERT +flips to the paper colour. + +--- + +## graph2.asm + +| Entry point | Description | +|---|---| +| [BLITZ / JBLITZ / FDMAINLP](#blitz) | Interpret a graphics-command string: coded PLOT/DRAW/CIRCLE/OVER/INK/CLS/PAUSE records | +| [FILL / JFILL](#fill) | Textured flood fill (optionally `USING a$` 16×8 pattern), using a check screen and a coordinate stack | +| [TRANSCR / TRANSODD](#transcr) | Copy the mode 2/3 screen into the &E000 "check screen" as a 1-bit-per-pixel blocked/free map | +| [CHARCOMP / GRCOMP](#charcomp) | Compress a mode 2/3 character cell to 1-bit form (SCREEN$, graphics COPY) | +| [GTFCOORDS / GTFIDFCDS / USCOORDS](#gtfcoords) | Unstack and range-check coordinates, applying XOS/YOS/XRG/YRG fiddling; force fat pixels for GET/ROLL | + +### Details + +#### BLITZ +`BLITZ a$` executes a string of records (the same format `GRAREC` appends +when RECORD is active): + +| Record bytes | Action | +|---|---| +| `sgnX, X, sgnY, Y` | Relative draw (sign bytes &00/&FF) | +| `01, x, y` | PLOT | +| `02, x, y` | DRAW TO | +| `03, x, y, r` | CIRCLE | +| `04, o` | OVER o | +| `05, i` | INK i | +| `06, c` | CLS c (0 = whole screen, 1 = window) | +| `07, n` | PAUSE n | + +Coordinates pass through the same fiddling as the typed commands. + +#### FILL +Requires MODE 2/3. Builds a 128-byte pattern buffer (solid = current ink; +`USING a$` = a 16-row pattern; string must be 128 bytes when patterned), +calls TRANSCR to build the check screen, then seed-fills line by line: for +each horizontal run bounded by set bits it marks the run, pushes candidate +runs above/below on FILLSTK, and paints the real screen through the pattern. +"Stored area too big"/stack overflow are guarded. + +#### TRANSCR +Copies the display (mode 3: whole screen; mode 2: the relevant part) to +&E000+ as one bit per pixel where 1 = colour differs from the fill origin's +colour — the fill's blocking map. + +#### CHARCOMP +Reduces an 8-scan character cell (2 or 4 bytes per scan) to 8 single bytes in +SCRNBUF by comparing against the top-left pixel colour — shared by SCREEN$ +matching and graphics COPY. + +#### GTFCOORDS +Unstacks Y then X, converts BASIC's −16…175 Y range to physical 0–191 +top-down, checks X against 255 (fat) or 511 (thin, CY returned), applying +the XOS/XRG/YOS/YRG pseudo-variables (which live at fixed displacements in +the numeric variables area). `GTFIDFCDS` halves thin X so ROLL/GRAB always +work in fat coordinates. + +--- + +## grabput.asm + +| Entry point | Description | +|---|---| +| [GRAB / JGRAB](#grab) | GRAB a$,x,y,w,len — capture a screen block into a string: `00,widthBytes,len` header + raw scans | +| [PUT / JPUT](#put) | PUT x,y,a$[,mask$] — write a grabbed block back with OVER 0–3/INVERSE, optionally masked | +| `GPTRUNC`/`GPVARS` | Clip block length at the screen bottom; compute screen address and counters | +| `PUTSRTAB` | The four 10-byte inner loops (LD/XOR/OR/AND) + fast LDIR path + masked path | +| `PSCHKMHL`/`SCRMOV` | Validate a PUT string (must start CHR$ 0, non-empty) and copy it into spare screen memory | +| [FARLDIR / FARLDDR / STRMOV](#farldir) | The universal cross-page block move: PAGCOUNT×16K + MODCOUNT bytes from page A,HL to page C,DE via a 256-byte bounce buffer | + +### Details + +#### GRAB +Captures via the ROLL/SCROLL store-block routine into RSBUFF, prefixing +control code 0, width (bytes) and length (scans), then assigns the whole +thing as a string (so it can be saved, copied, PUT elsewhere). Width is +rounded up to whole bytes; requires MODE 2/3. + +#### PUT +Validates the string(s); a mask string must match width/length ("PUT mask +mismatch"). Chooses the inner loop: OVER0+INVERSE0 uses plain LDIR; OVER 0–3 +apply INVERSE as an XOR mask then LD/XOR/OR/AND to the screen; the masked +variant ANDs through the mask string so only 1-bits of the mask are altered. +Blocks hanging off the bottom are truncated. + +#### FARLDIR +The ROM's memcpy. Source page/addr in A/HL, destination in C/DE, size in +PAGCOUNT (16K units) + MODCOUNT. Copies via BUFF256 in the system page in +256-byte chunks, re-paging source and destination alternately, in either +direction (FARLDDR for overlapping upward moves). Exit leaves TEMPW1/TEMPB2 +past the destination. INSLV offers a vector hook. + +--- + +## assign.asm + +| Entry point | Description | +|---|---| +| [VALFET1 / VALFET2](#valfet1) | Evaluate the right-hand side, insist its type matches FLAGS, then ASSIGN if running | +| [ASSIGN / ASSISR](#assign-1) | Store the FP-stack top into the variable described by DEST/DESTP/FLAGX/STRLEN | +| `ASENV`/`ASNN` | Overwrite an existing numeric / create a new numeric at NUMEND (linking it into its letter chain, opening 512 bytes if the gap is tight) | +| [ASSTR / ASDEL / ASNST](#asstr) | Assign to an existing string (copy + space-pad into the fixed-length slot, or delete-and-recreate for simple strings) / create a new string at the end of SAVARS | +| `ASDEL2`/`ASDEL3`/`ADD14` | Delete a string/array record (data + 14-byte header) | +| [RECORD](#record) | RECORD TO a$ (arm stream-16 output into a string) / RECORD STOP | +| [SYNTAX1 / SYNTAX4 / SYN14C](#syntax1) | Assess a variable for assignment (SYNTAX4: FOR variables), setting DEST/DESTP/STRLEN/FLAGX/DFTFB | +| [STKVAR / STKVAR2 / SVARRAYS / SLICING](#stkvar) | Index arrays and slice strings: subscript evaluation, element address computation, slicer defaults | +| [DIM](#dim) | DIM name(d1,…,dn): delete any old version, build header (total len, len, dim count, dim sizes) and clear the data | +| `GETSUBS` | Evaluate a subscript, check 1…limit, return it decremented | +| `SAROOM` | Open room at the end of SAVARS and copy in the type/name header | + +### Details + +#### VALFET1 +Used by LET/READ/INPUT: SCANNING evaluates, the result type is compared with +the target's (error 29 on mismatch), and in run time ASSIGN stores it. + +#### ASSIGN +Branches on type and FLAGX bit 0 ("new"): existing number → 5-byte copy over +the old value; new number → `ASNN` links a fresh record at NUMEND (relative +pointer from the previous chain tail) after ensuring ≥60 free bytes (else +opening 512 before SAVARS); strings → ASSTR. `CGXRG` is a related special: +halve/double XRG when FATPIX/MODE changes pixel width. + +#### ASSTR +Slices and array elements are fixed-size: the source is FARLDIR'd in, +truncated or space-padded to the destination length. Simple strings (DESTP +bit 7 set) are variable-size: the new value is first *created* as a fresh +record at the SAVARS end (`ASNST` — so `LET a$=a$+"x"` works), then the old +record is deleted (`ASDEL`). ASNST protects sources above WKEND from +pointer auto-adjustment during MAKEROOM. + +#### RECORD +`RECORD TO a$` deletes any existing a$, stores its name in STRM16NM, sets +GRARF (graphics commands also append their BLITZ records), and creates a$ as +a null string; stream 16 output (`S16OSR`) then appends bytes to it. +`RECORD STOP` clears GRARF. + +#### SYNTAX1 +LOOKVARS then `SYN14C`: records DEST (value address, or chain-tail/stopper +address for new variables), DESTP (page; bit 7 = "delete old copy"), STRLEN +(length or type byte), FLAGX bit 0 ("new"), and DFTFB (zero iff an existing +number is "minus zero", which DEFAULT treats as non-existent). Undimensioned +arrays and slices of new strings raise error 2 here. + +#### STKVAR +At check time just validates subscript/slicer syntax. At run time: simple +strings convert the pages+mod-16K length to a 16-bit length and stack a +descriptor (bit 7 of page set = replace-on-assign); arrays walk the dimension +list computing +\(\text{total} = (\dots(s_1 \times d_2 + s_2) \times d_3 + \dots) + s_n\), +finally multiplied by 5 (numeric) or by the last dimension (string) and +added to the data start. String results may then be sliced: +`SLICING` handles `(a TO b)` with defaults, empty result for reversed +ranges, and error 4 for out-of-range values. + +#### DIM +Deletes any existing variable of the same name, evaluates the dimension +sizes (pushing them on the machine stack and multiplying on the FP stack), +computes text size + dim-info size + 14-byte header, opens the room at the +SAVARS end via SAROOM, then writes: pages, len-mod-16K (of data after +header), dim count, dim sizes (words), and clears the data to 0 (numeric) or +spaces (string). Note the stored "length" covers everything after the +3-byte length field itself, i.e. dim data + elements. + +--- + +## fn.asm + +DEF FN / FN, DEF PROC / PROC / LOCAL, and the COMPILE pass. See +[tokenized-program-format.md](tokenized-program-format.md) for the calling +buffer byte format this file creates and patches. + +| Entry point | Description | +|---|---| +| [COMPILE / DOCOMP / SCOMP](#compile) | The compile pass: assign LABEL line numbers to variables, then patch every FN/PROC calling buffer with the target's page/address | +| `COMDF`/`COMDP` | Build a table of all DEF FNs (page/addr) in INSTBUF (≤170, error 52); scan for FE/FD calling buffers and resolve them | +| [LKCALL](#lkcall) | Find the next calling buffer: pattern `not-0E, 0E, FD/FD or FE/FE, byte≥&80`; extracts the call-site name into NMBUFF | +| [LOOKDF / LOOKDP](#lookdf) | Match the name against DEF FNs (table) / DEF PROC lines (LKFC scan); patch the buffer with page OR &80 + address, or &FF if unresolved | +| `MATCHER`/`MATCHFN`/`MATCHERF` | Case-insensitive name comparison (spaces skipped in the candidate) | +| [IMFN](#imfn) | Run-time FN evaluation: use the patched buffer to jump to the DEF FN, copy argument values into its parameter buffers, evaluate its expression with DEFADD set | +| [FNSYN / PROCSY / MKCLBF](#fnsyn) | Syntax-time: create the 6-byte calling buffer `0E FE FE FE ? ?` (FN) or `0E FD FD FD ? ?` (PROC) after the name | +| [MAKESIX](#makesix) | Open 6 bytes at (HL) and write the &0E marker — the primitive behind all invisible forms | +| [DEFPROC / PROCS](#defproc) | DEF PROC (run time: skip to END PROC) and the PROC-call dispatcher (reached for any statement starting with a letter) | +| [LOCAL](#local) | LOCAL v-list: process like PROC parameters with an empty call list, so variables become local | +| [LKFNVAR](#lkfnvar) | During FN evaluation, resolve single-letter variables from the DEF FN parameter buffers (searched via DEFADD) | +| `FORESP`/`FORESP1` | Advance HL to the next significant (≥ &21) character | +| [LKFC](#lkfc) | Find a token at the start of a program line (skipping leading spaces/CCs) — used for DEF PROC and LABEL scans | +| `FNNAME`/`VARNAME`/`VARAR` | Name syntax checks (FN names, PROC parameter names, `name()` forms) | + +### Details + +#### COMPILE +Runs before any program execution (RUN/CLEAR/GOTO of the edit line). If +COMPFLG bit 7 is set (program changed), it first executes every `LABEL name` +by assigning the line number to the named numeric variable, then resolves +calling buffers in the whole program; the edit line is always resolved (FNs +only if REFFLG says any FN appears). CHAD/KCUR juggling keeps pointers valid +while variable creation moves memory. + +#### LKCALL +Scans up to B' 8K blocks for &FD/&FE with CPIR, then verifies the full +signature: preceded by exactly one &0E (two would be a numeric literal +coincidence), followed by a byte with bit 7 set (fresh buffers hold FD/FE +&FD/&FE ≥ &80; patched ones hold page|&80). It then walks *backwards* over +the name (letters/digits/underscore/$, skipping a preceding &FF &42 'FN' +token pair) and copies length+name into NMBUFF. + +#### LOOKDF +FNs: compare NMBUFF against each table entry (the byte after 'DEF FN'). +PROCs: `LKFC` scans line starts for the DEF PROC token, then matches the +name. On success the buffer bytes 3–5 become page|&80 and the address just +past the DEF PROC/FN name; on failure page byte &FF (bit 5 set = "missing", +tested at call time). + +#### IMFN +Scans forward from CHAD to the &0E of the calling buffer, errors if bit 5 of +the page byte is set (7, "FN without DEF FN"), then for each parameter in +the DEF FN header: evaluates the corresponding FN argument (type-checked +$ vs numeric) and copies the 5-byte result into the parameter's own &0E +buffer inside the DEF FN line. Finally evaluates the expression after "=" with +DEFADD pointing at the DEF FN's parameter list (so LKFNVAR resolves +parameters) and CHAD/DEFADD restored afterwards — recursion-safe because the +previous DEFADD is stacked. + +#### FNSYN +At syntax-check time `FN name(args)` and `procname args` get `MKCLBF`: six +bytes are opened after the name — &0E then five filler bytes (&FE×3 for FN, +&FD×3 for PROC, last two undefined) — and the argument list is checked. +Because the buffer starts with &0E, every other part of the ROM (listing, +searching, skipping) treats it as an invisible number form. + +#### MAKESIX +`LD BC,6 : CALL MAKEROOM : LD (HL),&0E : INC HL : RET` — also used by DEF FN +parameter buffers and (in miscx2) DEF KEYCODE. + +#### DEFPROC +Running into a DEF PROC statement means falling through sequential execution: +it searches for END PROC (error 13 if missing) and continues after it. The +statement's syntax pass checks the parameter list (REF allowed) and an +optional trailing DATA. `PROCS` is the run-time call: finds the buffer, +errors 12 if unresolved, sets PRPTR (call-site parameter pointer) and +DPPTR (definition parameter pointer), pushes a PROC frame (&40) returning to +the *next* statement, then jumps to statement 2 of the DEF PROC line after +PROPAR has bound the parameters. + +#### LOCAL +Sets up PRPTR to point at a CR in ROM (empty call list) and DPPTR at its own +argument list, then runs the same parameter processing (PROP2), so each +listed variable is given a local (hidden-global) copy initialised from the +PROC machinery with no value. The current statement position is pushed as a +pseudo-PROC frame so END PROC unwinds it. + +#### LKFNVAR +Only single-letter names participate (longer names always use global +variables). Searches from DEFADD for parameter buffers: a numeric parameter +is `letter 0E …5 bytes…`, a string parameter `letter $ 0E …`; on a match the +5 bytes are stacked directly (for strings they are a string descriptor +copied from the argument). + +#### LKFC +Line-start scanner: for each line, skip leading spaces/control bytes; if the +first significant byte equals C, return NC with CHAD on it. Used by the +LABEL and DEF PROC passes and by RESTORE's DATA search variant. + +--- + +## nparpro.asm + +PROC parameter processing — the most intricate part of the interpreter. The +long comment at the top of the file is the specification; the summary: + +| Entry point | Description | +|---|---| +| [PROPAR / PROP2](#propar) | Bind PROC-call arguments to DEF PROC parameters, stacking undo records on the BASIC stack | +| [RESTORE / RESTOREZ](#restore) | The RESTORE command (placed here to share a JR); set DATADD to line n (or 0) | +| `UNVLK` | Find an "unused" numeric variable slot of matching name/type to re-use for a local | +| `RUAHL` | Record on the BASIC stack the displacement (from NVARS) of a variable to reveal/mark-unused at END PROC | +| `PPSUB` | Record a type/name string on the BASIC stack (with flag bits in the stored type byte) | +| `NEGVTR` | Cancel a "reveal" record when a REF variable turns out to be the same as a hidden global | +| [DELOCAL](#delocal) | END PROC/POP teardown: walk the BASIC-stack records — copy REF values back, mark locals unused, reveal hidden globals, delete local strings/arrays, rename REF strings back | +| `PTTODP`/`PTTOPR` | Point CHAD at the DEF PROC / PROC parameter list (Z at list end) | + +### Details + +#### PROPAR +For each DEF PROC parameter: numeric by value — any existing global of the +same name is hidden (bit 7 of its type byte; a second copy is neutralised as +"minus zero"), a local is created (re-using an "unused" slot when possible) +and the call argument assigned to it. REF numeric — the argument must be a +variable; the local is aliased both ways with records so END PROC copies the +final value back. Strings/arrays by value — the global is hidden and a new +local string created at the SAVARS end; REF strings/arrays are *renamed* to +the DEF PROC name via the rename stack at HDR, and renamed back at END PROC. +Every action pushes a typed undo record; "BASIC stack full" (41) and +"Parameter error" (26) guard the process. + +#### RESTORE +`RESTORE [n]` finds line n (default 0) and points DATADD/DATADDP just before +it so READ's scanner takes over from there. + +#### DELOCAL +Interprets the undo records (format documented in the source at DELOCAL): +terminator 00 00; &FF-prefixed = REF string rename record; bit 4 set = +string/array name (bit 7: also reveal a hidden global); bit 4 clear = +displacement of a numeric (bit 7: reveal; bits 7/5: REF value copy-back via +a temporary buffer). Runs until the terminator, restoring BSTKEND. + +--- + +## misc2.asm + +| Entry point | Description | +|---|---| +| [ERROR2](#error2) | The RST 8 back-end: record XPTR, consult RST8V, pass hook/error codes to DOS if booted, else set ERRNR and long-jump to ERRSP | +| `PTDOS`/`DOSC` | Page the DOS in at &4000 and call its error (&4203) / hook (&4200) entries | +| [TOKMAIN / TOKDE / BUFMV](#tokmain) | Copy the tokenizer body from ROM1 into CDBUFF+&80 and run it over the edit line (or arbitrary text for VAL) | +| `MEPROG`/`INPUT`/`RENUM`/`KEYIN`/`GET`/`DELETE`/`POP`/`DEFKEY`/`DEFFN` | Stubs that copy the corresponding ROM1 body (see miscx1/miscx2) into INSTBUF/HDR and jump to it | +| [LET / DEFAULT](#let) | LET a=…,b=… and DEFAULT (assign only if the variable doesn't exist / is "minus zero") | +| `LABEL`/`SVNUMV`/`VNUMV` | LABEL name: (run time: skip; compile does the work); numeric-variable validators | +| [RUN / CLEAR](#run) | RUN [n] = GOTO n + RESTORE 0 + CLEAR 0; CLEAR [addr] resets variables, stacks, RAMTOP | +| `CLRSR`/`SETSYS`/`SETNE`/`SETSAV` | Clear FPCS/BASIC stack/variables (26 chain pointers to FFFF, XYZ pseudo-vars re-seeded); set (page,addr) sysvars | +| `CLSND` | Zero all 32 sound-chip registers | +| [S16OSR](#s16osr) | Stream-16 output: append the character to the string variable named in STRM16NM | +| [SYNTAX3 / SYNTAX6 / SYNTAX8 / SYNTAXA and friends](#syntax3) | Argument-shape helpers: expect number/0, number, number-pair, string; bracket/comma insisters; expression classifiers | +| `RUNFLG`/`ABORTER`/`CHKEND` | CY if running; abort a command routine at check time; insist on end-of-statement | +| [ALPHA / NUMERIC / ALPHANUM / ALDU / ALNUMUND](#alpha) | Character classes (ALDU defines which trailing chars block tokenization) | +| [BRKLSSL / GIR / GIR2](#brklssl) | Bracketless slicer `a TO b` for LIST/DELETE/AUTO ranges into FIRST/LAST | +| `DISPLAY`/`SETDISP`/`VIDSEL`/`SDISR` | DISPLAY n: switch the displayed screen, swapping palettes between screen pages | +| `PRSVARS`/`RSVARS`/`SSVARS` | Save/restore the print variables block to/from the non-displayed screen's page | +| [R0INST](#r0inst) | The INSTR inner search (CPIR-based, `#` wildcard) | + +### Details + +#### ERROR2 +Records CHAD in XPTR (the `?` marker for listings), fetches the code byte +following the RST 8, and: if DOS is booted and not already in control, passes +hook codes (≥128) and errors to the DOS page (special returns 1/2/3 continue +LOAD/SAVE flows); codes ≥128 without DOS give "No DOS" (53). Otherwise ERRNR +is set and SP reset from ERRSP — landing in MAINER, an editor error frame, or +a SETESP frame. + +#### TOKMAIN +The tokenizer must run with ROM1 paged out (the text lives up there in the +same range), so its body (`TOKPT2`, miscx2.asm) is LDIR'd from its assembled +position in ROM1 to CDBUFF+&80 and entered there. `TOKDE` is the entry used +by VAL/VAL$, tokenizing arbitrary text at DE. All the sibling stubs compute +their body's ROM1 address as &C000 plus the summed lengths of the bodies +before it. + +#### LET +LET assigns each `var=expr` via SYNTAX1+VALFET1. DEFAULT does the same but, +when running and the variable exists (DFTFB≠0, i.e. not "minus zero"), the +expression is evaluated and discarded instead of stored. + +#### RUN +RUN = GOTO n (default 0), RESTORE 0, then CLEAR's core: reclaim the +NVARS→ELINE gap to 605 bytes, clear variables/stacks (CLRSR), re-run COMPILE, +CLS, and validate/set RAMTOP (error 48). CLEAR addr moves RAMTOP (checked +against WKEND+180 and LASTPAGE). + +#### S16OSR +Looks up the recorded string variable, extends it by one byte at its end +(MKRM1) and stores the output character — making `PRINT #16` (and RECORDed +graphics) append to the string. Error 42 past &FEFF bytes. + +#### SYNTAX3 +The syntax helper family: each expects a particular argument shape and, at +check time, *discards its caller's return address* so the command routine is +skipped (the standard early-out idiom). EXPTEXPR returns Z for a string +expression; EXPT1NUM/EXPT2NUMS/EXPT4NUMS chain commas; SYNTAX9 handles +embedded colour items before coordinates. + +#### ALPHA +CY for A–Z/a–z. ALDU (letter, `_` or `$`) is what the tokenizer checks after +a matched word — so `printer`, `print_out`, `print$` stay untokenized while +`print1`/`print:` tokenize. + +#### BRKLSSL +Parses `n`, `n TO`, `TO m`, `n TO m`, or nothing into FIRST/LAST (defaults +1/&FEFF), returning A=0 when a single number was given (LIST turns that into +"n TO end"; DELETE keeps it as one line). + +#### R0INST +The INSTR core: CPIR for the first target character, then byte compare with +the INSTHASH wildcard honoured; returns BC=1-based position or 0/CY. + +--- + +## endprint.asm + +| Entry point | Description | +|---|---| +| [EPSUB / M0PRINT / M1PRINT / M2PRINT / M3PRINT](#epsub) | Render one character cell in each mode from the pattern at HL, honouring OVER (B) and INVERSE (C) masks and CSIZE | +| `POPOUT`/`R1OSR` | Paging bracket used around ROM0 helpers called from ROM1 | +| `GTRLNN` | RENUM helper: read the line number a header points at | +| [CWKSTK](#cwkstk) | Copy BC bytes from common memory to the workspace and stack the string parameters | +| [POFETCH](#pofetch) | Fetch the current print position (upper/lower screen or printer) and RHS limit | +| `UTMSG`/`POMSG` | Print utility message A / message A from list DE | +| [ANYDEADDR / M0DEADDR…M3DEADDR / CLCPO](#anydeaddr) | Row/column → screen address for each mode (mode 2 returns 6-pixel odd/even info) | +| [ANYPIXAD / M0PIXAD / M1PIXAD](#anypixad) | Pixel coordinate → address + bit/nibble offset for each mode | +| [POATTR01 / SETATTR](#poattr01) | Write the attribute for a mode 0/1 cell from ATTRT/MASKT/PFLAGT (INK 9/PAPER 9 contrast logic) | +| [STRCOMP](#strcomp) | Compare the two strings on the FP stack (Z equal, CY S1255 (SBFSR2 variant +allows 511), copy to INSTBUF. + +#### TSTRMBIG +All allocation funnels here: computes new WKEND, compares against RAMTOP, +raising "Out of memory" (1) if it doesn't fit; TESTROOM is the BC-byte +convenience wrapper. + +--- + +## miscx1.asm + +Start of ROM1. The first several bodies are `ORG`'d at their RAM execution +addresses (INSTBUF or HDR) and copied there by the misc2.asm stubs before +running, because they must read/write the program area that occupies the same +addresses as ROM1. + +| Entry point | Description | +|---|---| +| [RNMP2 (RENUM)](#rnmp2-renum) | RENUM [first TO last] [LINE l] [STEP s]: build an old→new line-number table in screen memory, rewrite line numbers and every reference | +| `CHGREF`/`RENTAB` | Rewrite line-number references after tokens in RENTAB (DELETE, ON ERROR, LINE, LLIST, LIST, RESTORE, GOTO, GOSUB, RUN), re-sizing each line via ADJLINE | +| `ADJLINE` | Open/close space inside a program line and fix its length field | +| `TRANSFORM`/`TRANSHL` | Map an old line number through the SBO/SBN tables to its new value | +| [GETP2 (GET)](#getp2-get) | GET var: wait for a key and assign it (digit/letter value for numerics, CHR$ for strings) | +| [DELPT2 (DELETE)](#delpt2-delete) | DELETE [n] TO [m]: reclaim the block of lines | +| [KEYP2 (KEYIN)](#keyp2-keyin) | KEYIN a$: place a string in the edit line, tokenize, syntax-check, then insert or execute it | +| [POPP2 (POP)](#popp2-pop) | POP [var]: pop any BASIC-stack frame, optionally assigning the return line number; PROC frames also run DELOCAL | +| [INPP2 (INPUT)](#inpp2-input) | INPUT [LINE] items: print prompts, edit the reply in workspace, validate/assign; INPUT LINE takes raw text | + +### Details + +#### RNMP2 (RENUM) +Defaults LINE 10 STEP 10 over the whole program. Uses the two screen-page +halves SBO/SBN as parallel arrays of old and new numbers (needs 6K free and +the screen paged in), refusing if lines would collide with un-renumbered +neighbours or exceed &FEFF. Reference rewriting evaluates the &0E form after +each reference keyword, transforms it, writes the new 5-byte form and +re-prints the digits (via `JPFSTRS`), adjusting the line length either way — +so both the visible text and the invisible form stay consistent. + +#### GETP2 (GET) +Waits for a key (BREAK allowed). String targets get CHR$(key); numeric +targets get 0–9 for digits or 10+ for letters (hex-style). + +#### DELPT2 (DELETE) +Range via GIR2/`TO`; unlike LIST, a single number deletes one line. Computes +the page-form length between the first and past-the-last line and RECL2BIGs +it, then re-enters execution via GT4R (a GOTO to the current position, since +NXTLINE etc. are stale). + +#### KEYP2 (KEYIN) +The programmatic line-entry command: the string is copied into ELINE, +TOKMAIN+LINESCAN run over it, and it is inserted (line number present) or +executed as a direct command (no line number) — all under a private error +frame so failures return to the caller. + +#### POPP2 (POP) +`POP` discards the top BASIC-stack frame of *any* type; `POP v` also assigns +the frame's return line number (0 for the edit line) to v. PROC frames get +their locals unwound. + +#### INPP2 (INPUT) +Clears the lower screen and workspace, then walks the item list: embedded +`(print items)`, prompts, separators, and variables. Each numeric/string +variable presents an editing buffer (quotes pre-inserted for plain string +INPUT), runs the EDITOR on it, tokenizes, and validates with a syntax pass +before assigning; INPUT LINE skips validation and assigns the raw text. +`STOP` typed as input raises error 17. Errors in K/S channels loop back to +re-edit rather than aborting. + +--- + +## miscx2.asm + +| Entry point | Description | +|---|---| +| [DKP2 (DEF KEYCODE)](#dkp2-def-keycode) | DEF KEYCODE n,a$ or DEF KEYCODE n:statements — store a definition in the DEF KEY buffer | +| [DFNP2 (DEF FN)](#dfnp2-def-fn) | The DEF FN statement's syntax pass: create the &0E parameter buffers after each parameter name | +| [TOKPT2 (the tokenizer)](#tokpt2-the-tokenizer) | Convert spelled-out keywords in a line to token bytes (runs at CDBUFF+&80) | +| [MEPRO2 (MERGE)](#mepro2-merge) | MERGE: load a program file into workspace and merge lines/variables into the current program | + +### Details + +#### DKP2 (DEF KEYCODE) +Key codes 192–254 are definable. The definition is either a string or the +raw rest of the line (statements form). Any existing definition is closed +up; the new one is appended before the &FF terminator as `code, len16, text` +(error 52 if DKLIM would be passed). + +#### DFNP2 (DEF FN) +Runs only at syntax time (running skips the statement). For each parameter +`letter[$]`, `MAKESIX` opens `0E xx xx xx xx xx` right after the name — +the buffer that IMFN will fill with the argument value at call time. Then +the result expression after `=` is type-checked against the FN name's type. + +#### TOKPT2 (the tokenizer) +The full algorithm is documented in +[tokenized-program-format.md](tokenized-program-format.md#the-tokenizer). +In brief: scan the line; at each candidate word start (letter, `<`, `>`) +copy up to 15 characters to a scratch buffer and match against the keyword +table with GETTOKEN (plus the MTOKV user hook); on a match write either a +single token byte (&85–&FE, entries ≥ &4A) or an &FF prefix + function code +(&3B–&83), absorb one leading and one trailing space, close up the freed +text, and continue — stopping at CR and after REM, and skipping quoted +strings and `FF`-prefixed codes already present. + +#### MEPRO2 (MERGE) +Loads the file into an opened workspace block, then: merges BASIC lines one +by one (replacing same-numbered lines via MAKEROOM/FARLDIR), merges numeric +variables (re-creating each via the FP stack), and merges strings/arrays +(deleting same-named victims first). Uses the saved header's three length +fields to separate program, numeric variables and string area. + +--- + +## fpcmain.asm + +The floating-point calculator: a byte-coded stack machine entered by RST &28. +The opcode values are listed in [constants.md](constants.md#fpcmainasm). + +| Entry point | Description | +|---|---| +| `FPATAB` | Dispatch table: operation code ×2 indexes the handler address | +| [FPCMAIN / FPCLP / BREGEN](#fpcmain) | The fetch-execute loop: IX is the instruction pointer, DE tracks STKEND; RST28V hook first | +| `FPUSEB` | Execute the operation code held in BREG (how the expression evaluator runs queued operators) | +| `FPSTO`/`FPSTOD`/`FPRCL` | Store (keep/delete) and recall calculator memories 0–5 at (MEM) | +| [FPCONST / FPCTAB](#fpconst) | Stack a constant (0.5, 0, 16384, 1.0, 1, 10, π/2) | +| `FPEXIT`/`FPEXIT2` | Leave the calculator (EXIT2 also unwinds the RST 28 frame — used at the end of `DB CALC` sequences inside FPC routines) | +| `FPDUP`/`FPDROP` | Duplicate / drop the top entry | +| [FPJUMP / FPJPTR / FPJPFL / FPLDBREG / FPDECB](#fpjump) | Signed relative jumps in the code stream, conditional on true/false; BREG load/decrement-and-branch | +| `FPSOMELIT`/`FP5LIT`/`FP1LIT` | Stack inline literals (n bytes / 5 bytes / 1 byte as a small integer) | +| `FPLKADDRB`/`FPLKADDRW` | PEEK/DPEEK an inline address onto the stack | +| [FPGRTE0 / FPGRTR0 / FPNOT / FPLESE0 / FPLESS0 / SETTRUE / SETFALSE](#fpgrte0) | Sign/zero tests producing 1/0 | +| `FPAND`/`FPOR`/`FPSAND` | Logical AND/OR (N2 zero-test semantics); `a$ AND n` | +| [FPSEQUAL…FPSNOTE](#fpsequal) | The six string comparisons via STRCOMP | +| [FPNEQUAL…FPNNOTE](#fpnequal) | The six numeric comparisons as subtract + sign-test calculator programs | +| `FPMOD`/`FPIDIV` | MOD and DIV as composite calculator programs | +| `FPBOR`/`FPBAND` | Bit-wise OR/AND on 16-bit integers | +| `TSTZERO` | Z if the 5-byte value at (DE) is zero | + +### Details + +#### FPCMAIN +Each iteration pushes FPCLP as the handler's return, refreshes STKEND from +DE, fetches the code byte at (IX+0). Codes &00–&1F are binary (DE backed up +5 so HL/DE point at N1/N2 and the result overwrites N1), &20–&5F unary, +&C8–&CF/&D0–&D7/&D8–&DF store/store-keep/recall memories, &E0–&FF constants. +Anything else: error 51 "FPC error". The RST28V vector sees every code +first, allowing full replacement/extension. + +#### FPCONST +FPCTAB packs the constants: small-integer forms for 0, 16384, 1, 10 and FP +forms for 0.5 (&80 00 00 00 00 → exponent-only), 1.0, π/2 +(&81 49 0F DA A2). + +#### FPJUMP +The displacement byte at (IX) is signed. JPTRUE/JPFALSE are *binary* +operations: they drop the tested value as they branch. + +#### FPGRTE0 +The comparison-with-zero family reads the sign byte and/or zero-tests the +value in place, then overwrites it with small-integer 1 or 0 (`SETTRUE`/ +`SETFALSE` → `STACKC` writes 00 sign C 00 x). + +#### FPSEQUAL +All six call `STRCOMP` (endprint.asm) then map Z/CY onto true/false. + +#### FPNEQUAL +Each numeric comparison is literally `SUBN` followed by the appropriate +sign/zero test — three-byte calculator programs. + +--- + +## transend.asm + +Chebyshev-based transcendental functions, all written as calculator code. + +| Entry point | Description | +|---|---| +| `FPSIN`/`FPCOS`/`FPTAN` | Sine (W.E. Thomson's faster series), cosine = sin(x+π/2), tan = sin/cos | +| `FPREDARG` | Reduce an angle to the −π…π range (V = x/2π fractional part scaled) | +| `FPEXP`/`FPPOWR2` | \(e^x = 2^{x \log_2 e}\); \(2^y\) with integer/fraction split (error 28 on overflow, 0 on deep underflow) | +| `FPPOWER` | \(N_1^{N_2}\): special-cases \(N_1 = 0\) and integer powers 0–&3F (by repeated multiplication), else \(e^{N_2 \ln N_1}\) | +| `FPLOGN` | Natural log: exponent extraction + series on the mantissa | +| `FPARCTAN`/`FPARCSIN`/`FPARCCOS` | ATN (range-folded series); \(\operatorname{ASN} x = \operatorname{ATN}\dfrac{x}{\sqrt{1-x^2}}\); \(\operatorname{ACS} x = \pi/2 - \operatorname{ASN} x\) | +| [SERIES](#series) | The Chebyshev series engine: 12-coefficient loop driven by DECB with inline literals | +| `FPSQR` | (in rom1fns/transend flow) \(\sqrt{x} = x^{0.5}\) via the POWER path with a fast exponent halving | + +### Details + +#### SERIES +Generator for all the above: BREG counts 12 terms; each loop iteration +stacks a coefficient literal (variable-length via SOMELIT) and performs the +Chebyshev recurrence in memories 0–2. The polynomial argument must be +pre-scaled by the caller. + +--- + +## mult.asm + +The binary arithmetic core. + +| Entry point | Description | +|---|---| +| [QMULT](#qmult) | Fast 16-bit HL×DE (used by array indexing etc.), CY on overflow | +| `STORADE`/`STOREI`/`FETCHI` | Small-integer store/fetch between (HL) and DE/C(sign) | +| [FPMULT / FPMULT2](#fpmult) | Multiply: integer×integer fast path (falling back on overflow), power-of-two shortcut, else 32×32-bit mantissa multiply with rounding | +| [FPDIVN](#fpdivn) | Divide: power-of-two shortcut, else 34-bit restoring division | +| [FPADDN / FPSUBN](#fpaddn) | Add/subtract: integer fast path, else align exponents (ADDALIGN), add/sub mantissas, normalise (MULNORM) and round | +| `MUDIADSR` | Unpack two FP numbers into registers for multiply/divide (restores implicit leading 1 bits) | +| [DFPFORM / FPFORM](#dfpform) | Convert one/both stack entries from small-integer to full FP form (RESTACK) | + +### Details + +#### QMULT +Chooses the smaller operand as the bit counter; ~8 shifts for byte operands. + +#### FPMULT +Both-integer inputs try QMULT with sign handling; overflow re-enters the FP +path. In FP: sign = XOR of signs, exponents add (−&80 bias), mantissas +multiply 8 bits at a time into HL'HL, then MULNORM normalises (shifting up +to 32 bits for cancellations), rounds on the 33rd bit, and stores exponent + +31-bit mantissa with the sign in bit 7 of the first mantissa byte. Overflow +→ error 28; underflow → zero. + +#### FPDIVN +Division by zero → error 28 (via the subtract in the exponent path checking +N2=0 first — "Number too large"). Restoring division produces 34 bits so +the 0.25–0.999 raw quotient can be normalised and rounded. + +#### FPADDN +If both are small integers, a 16-bit add with sign logic handles it unless +it overflows. FP path: the smaller exponent's mantissa is shifted right by +the exponent difference (>32 → result is the larger), mantissas add (same +sign) or subtract (different), the result renormalises — including the full +cancellation case giving zero. + +#### DFPFORM +`FPFORM` (= calculator op RESTACK) rewrites a small-integer entry +`00 sign lo hi 00` as exponent/mantissa form (exponent = \(\&90 - s\) where +\(s\) is the normalising shift; mantissa = the value shifted so bit 30 is +the top set bit; sign in mantissa bit 7). +`DFPFORM` does both operands for the multiply/divide/power paths. + +--- + +## rom1fns.asm + +| Entry point | Description | +|---|---| +| [FPVAL / FPVALS](#fpval) | VAL/VAL$: tokenize the argument text in workspace, syntax-check it as an expression, then evaluate it (with FLAGS run bit borrowed) | +| `IMRND` | RND [(n)]: congruential seed update \(\text{seed} \leftarrow ((\text{seed}+1) \times 75 \bmod 65537) - 1\); returns a fraction, or an integer in \(0 \dots n-1\) | +| `IMATTR`/`IMPOINT` | ATTR(l,c) and POINT(x,y) — read attribute byte / pixel ink number in any mode | +| `GETCP` | Validate a line/col pair against limits | +| `FPINKEY`/`FPINKEN` | INKEY$ [#n]: stream version reads the channel; builds a 1-char string | +| `FPBUTTON` | BUTTON n — mouse button status bit | +| `FPSVAR` | SVAR n = &5A00+n (address of a system variable) | +| `FPCHRS` | CHR$ n — 1-byte string in workspace | +| `FPBINS`/`FPHEXS` | BIN$ (8/16 digits, using BIN1DIG/BIN0DIG chars), HEX$ (2/4/6 digits by magnitude) | +| `MEMRYSP2` | MEM$(a TO b) part 2: copy the memory range to workspace as a string | +| [FPCONCAT](#fpconcat) | String + string: copy both into fresh workspace (error 42 past 64K−1) | +| [AMPERSAND](#ampersand) | `&hex` literal parser (up to 6 digits, 19-bit + sign forms) — evaluated at syntax time into the 5-byte form | +| `FPUSRS`/`FPUSR` | USR$/USR — trampoline to ROM0's CALLX | +| `FPPEEK`/`FPDPEEK` | PEEK/DPEEK through PDPSUBR's 0–512K addressing | +| `FPTRUSTR` | TRUNC$ — strip trailing spaces | +| `FPSTRS`/`FPCODE`/`FPLEN`/`FPUDG` | STR$ (via PFSTRS), CODE, LEN, UDG address | + +### Details + +#### FPVAL +The remarkable one: VAL copies its string to workspace with a CR appended, +runs **TOKDE** (the tokenizer) over it, then SCANSR twice — once with the run +bit forced off (a genuine syntax check, which also inserts 5-byte forms into +the workspace copy), once running to get the value. So VAL accepts anything +an expression can be, including keywords in text form. VAL$ is the string +flavour. + +#### FPCONCAT +Fetches S2's descriptor, opens len1+len2 workspace bytes, FARLDIRs both +halves in, stacks the result descriptor. + +#### AMPERSAND +Called from CALC5BY during syntax checking: accepts 1–6 hex digits +(letters case-insensitive), building a 19-bit-plus value; results ≤ &FFFF +become small integers, larger values full FP. Also accepts the `nnnnH` +suffix form. + +--- + +## scrsel1.asm + +| Entry point | Description | +|---|---| +| [SCREEN / JSCRN](#screen) | SCREEN n: make screen n current for output (display too if DISPLAY 0) | +| `SCRNTLK2` | Look up screen 1–16 in SCLIST → mode/page (Z if closed) | +| [OPSCRN / CLSCRN](#opscrn) | OPEN SCREEN n,m / OPEN #s,"letter" / OPEN n pages; CLOSE SCREEN n / CLOSE #s / CLOSE n pages — page allocation in ALLOCT | +| `CHLTCHK` | Map a channel displacement to its letter and class | +| [INTS](#ints) | The interrupt demultiplexer: line, frame, MIDI in/out, mouse/COMs — each via its vector | +| [FRAMINT](#framint) | Frame interrupt: palette flash (two palette tables swapped every SPEEDINK frames), line-interrupt table reload, FRAMES counter, keyboard scan, screen-off counter | +| [LINEINT](#lineint) | Line interrupt: walk LINICOLS entries `scan,palette-entry,colour1,colour2`, reprogramming the CLUT at exact scans | +| [KEYRD2 / KINTER / KEYSCAN…](#keyrd2) | Keyboard scanning into the 8-byte queue with rollover, repeat (REPDEL/REPPER), shift/symbol/control translation through KTAB | + +### Details + +#### SCREEN +Selects the mode/page pair from SCLIST into CUSCRNP (print/plot target) and, +if no explicit DISPLAY is active, also shows it. Print variables of the old +screen are saved into its second page (PVBUFF) and the new screen's set +loaded — each open screen keeps its own windows, positions and colours. + +#### OPSCRN +`OPEN SCREEN n,m[,c]` allocates 2 pages (marked &40 in ALLOCT) and enters +mode/page into SCLIST — error 44 if the screen exists, 1 if no pages. +`OPEN #s;"x"` binds stream 4–15 to a channel letter via CLTAB. `OPEN n` / +`OPEN TO n` reserve raw pages for the user (adjusting LASTPAGE). CLOSE +reverses each form; closing the displayed screen falls back to screen 1. + +#### INTS +Reads the pending-interrupt bits saved by the RST &38 stub and dispatches in +priority order line → frame → MIDI-out → COMs → MIDI-in, each first offering +its vector (LINIV/FRAMIV/MOPV/COMSV/MIPV). + +#### FRAMINT +Every 20 ms: reload the line-interrupt pointer to the LINICOLS table start +and program the first entry; every SPEEDINK frames swap PALFLAG and load the +other palette table (this is how FLASH colours work — the two 16-entry +tables at PALTAB differ only in flashing inks); increment FRAMES (3+2 +bytes); call the mouse vector; scan the keyboard (KEYRD2); count down +SOFFCT for automatic screen-off. + +#### LINEINT +LINICOLS entries are 4 bytes: scan line, palette index, colour A, colour B +(A/B by PALFLAG). The handler spins on STATPORT to hit the exact scan, +reprograms the CLUT entry, loads the next entry's scan into the line +interrupt register, and returns — up to 127 changes per frame. + +#### KEYRD2 +Full-travel scan of the 9 half-rows into a 72-bit map; two-key rollover with +the shift keys read separately; new keys translate through the three 69-byte +KTAB planes (plus the control-key table) and are queued in KBQB (8 deep, +head/tail in KBQP); repeats after REPDEL then every REPPER frames. LASTK and +FLAGS bit 5 present the queue head to the interpreter. + +--- + +## scrsel2.asm + +| Entry point | Description | +|---|---| +| [GOTO / GOSUB / GOTON](#goto) | GOTO/GOSUB n, and the `GOTO ON x;l1,l2,…` selector form | +| [GETTOKEN](#gettoken) | The keyword matcher used by the tokenizer (public vector &018A) | +| [MODPT2](#modpt2) | The MODE switch: set MODE/CUSCRNP/hardware, window geometry (MDSR), expansion tables, mode 2↔3 colour juggling, then CLS | +| `FATPIX` | FATPIX 0/1 — switch mode 2 pixel width, rescaling XCOORD and XRG | +| `WIDTH` (CSIZE) | CSIZE w,h — character cell size (6/8 wide, 6–32 high; ≥16 = double height) | +| `SUET`/`QUADBITS`/`DBBITS` | Build EXTAB: each 4-bit pattern doubled (mode 2) or quadrupled (mode 3) | +| [AUTO](#auto) | AUTO line,step (both optional): arm AUTOFLG/AUTOSTEP and re-enter the main loop via AULL | +| [SOUND](#sound) | SOUND reg,val;reg,val;… — batch writes to the sound chip (≤127 pairs) | +| [BOOT / BOOTEX](#boot) | BOOT: find/reuse a DOS page, read track 4 sector 1, verify the "BOOT" signature, jump to &8009 | + +### Details + +#### GOTO +GOSUB stacks a GOSUB frame (line number range-checked) before the shared +GOTO3 which sets NEWPPC/NSPPC=0. `ON x;` picks the x'th number from the +list (out-of-range falls through to the next statement). + +#### GETTOKEN +Inputs: HL = keyword list −1, A = word count +1, DE = the candidate text +(copy). Scans the list (last letter of each word has bit 7 set), matching +case-insensitively; embedded spaces in list words ("GO TO", "DEF FN", +"END PROC", "LOOP IF"…) are optional in the input. After a full match the +next input character must not be a letter/underscore/`$` (via ALDU) unless +the word ends in `=`, `>` or `$` (so `<>`, `<=`, `>=`, `CHR$a` work). +Returns A = 1-based index of the matched word, Z if none. + +#### MODPT2 +Mode 0 gets 8-pixel-high cells (attribute alignment), modes 1–3 get 9. Mode +2 selects 64 or 85 columns by FL6OR8. Switching to/from mode 2 saves and +restores the mode 3 paper/lower-screen colours and converts between striped +and solid formats, doubling/halving X coordinates when the thin-pixel flag +changes. + +#### AUTO +Defaults: line = EPPC+10, step 10. Stores step and EPPC=line−step, then each +pass of the main loop calls AULN (editor.asm) to pre-type the next number. + +#### SOUND +Collects register/value pairs (registers 0–31) into INSTBUF, terminates with +&FF, then squirts them to ports &1FF/&FF. + +#### BOOT +`BOOT 1` forces a re-boot; plain BOOT with DOS resident just issues the +auto-load hook. Otherwise: find a free (or previous DOS) page, spin up the +drive checking the index hole ("Missing disc"), step to track 4, read sector +1 to &8000 with retries, check bytes &8100+ spell "BOOT" ("No DOS" +otherwise), and jump to the DOS init at &8009. + +--- + +## printfp.asm + +| Entry point | Description | +|---|---| +| [PFSTRS](#pfstrs) | STR$/PRINT of the number on the FP stack: BC digits at (DE) in PRNBUFF, rounded to 8 significant digits, E-form when needed | +| [PRFPBUF](#prfpbuf) | Raw conversion: integer part by repeated decimal subtraction/BCD, fraction by repeated ×10 of the binary fraction | +| `DECDIGP`/`DECDIGN` | log10 bounds for a power of two (digits before point / zeros after) | +| `DECIMIZE`/`PRBCD` | Binary→BCD conversion of the integer part and BCD→ASCII output | +| [POFTEN](#poften) | Multiply the FP top by \(10^{A}\) (A signed) — used by E-format input and output | + +### Details + +#### PFSTRS +Drives PRFPBUF then post-processes: round the 9th digit up (with carry +rippling and possible re-lengthening), decide E-format (magnitude ≥ 1E8 or +leading zeros beyond FRACLIM), place the decimal point, strip trailing +zeros, and emit `mantissa E±nn` when in E-form. FRACLIM (normally 6) allows +up to 4 leading fraction zeros before switching to E-form. + +#### PRFPBUF +Splits the number at the binary point using the exponent; the integer part +is converted through a 5-byte BCD buffer (up to 10 digits), the fraction by +multiplying the remaining bits by 10 repeatedly, taking the carry-out digit +each time until 9 digits exist. Numbers ≥ 2^32 are pre-scaled by negative +powers of ten (recording EPOWER). + +#### POFTEN +Squares 10 progressively (binary exponentiation over the bits of |A|), +multiplying or dividing the target as bits demand — also the engine behind +`1.23E-4` input scaling. + +--- + +## tprint.asm + +The output side of the token system, plus control codes. `PROM1` is where +channel K/S output lands (via PRMAIN in ROM0). + +| Entry point | Description | +|---|---| +| [PROM1 / PRASCII](#prom1) | Main print: <&20 control codes, &20–&7F ASCII through CHARS, ≥&80 tokens/UDGs | +| `NLENTRY`/`PRNONWLN` | Position bookkeeping: line-full → recursive CR (with listing indent), then render via PATOUT | +| [PRGR80 / PRGR802](#prgr80) | Codes ≥&80: UDGs when in quotes/INPUT (or &80–&84 always); otherwise keyword expansion via the four sub-lists | +| [POFN / PSTFF2](#pofn) | &FF prefix: capture the next byte, then print the named function with correct spacing rules | +| [POMSR / POMSR3 / MVWORDLP](#pomsr) | Message/keyword extraction into MSGBUFF: walks bit-7-terminated lists, expanding compression codes 0–31 recursively | +| [POUDG / PUDGS](#poudg) | Block graphics (&80–&8F computed from the code's nibbles) and UDG/foreign characters via UDG/HUDG pointers | +| [PRCRLCDS / CCPTB](#prcrlcds) | Control codes 6–23: comma-tab, cursor moves, delete, ENTER, colour codes (collect parameters by channel redirection), AT/TAB | +| `PRENTER`/`LPRENT` | CR handling with window scroll; printer CR+optional LF | +| [SCRLSCR / SCRLS](#scrlscr) | "scroll?" prompting, autolist abort, lower-screen scroll stealing rows from the upper screen | +| `DBCHAR` | Double each scan of a character matrix for double-height printing | +| `EROC2` | Erase the old `>` cursor (listing support) | +| `WTBRK` | Prompt via a utility message and wait for a key | + +### Details + +#### PROM1 +ASCII: pattern address = (CHARS)+8×code; FLAGS bit 0 tracks "last char was a +space" for keyword spacing. All rendering funnels through NLENTRY (position/ +line-full logic) then the PATOUT vector (normally ENDOUTP → per-mode +renderer in endprint.asm; DMPFG discards output for measuring). + +#### PRGR80 +The INQUFG (in-quotes) and FLAGX (INPUT) flags force UDG printing so program +*strings* list as characters while program *code* lists as keywords — +resolving the &85–&FE overlap between tokens and UDGs by context. PRGR802 is +also called by the editor's channel R to expand tokens into the edit buffer. + +#### POFN +Printing an &FF redirects the channel to capture the following code byte, +then selects the token sub-list (immediate/FPC/binary) and spacing: MOD–AND +get spaces both sides; FN and BIN a trailing space only; `<>`, `<=`, `>=` +none; keywords ending in a letter or `$` get a trailing space; a leading +space is added unless the previous character was one. + +#### POMSR +The list walker shared by keywords, error messages and utility messages: +entry N is found by counting bit-7 bytes; bytes &00–&1F recurse into +COMPLIST (the compression fragments); the expansion accumulates in MSGBUFF +with BC=length. + +#### POUDG +Codes &80–&8F with BGFLG=0 are synthesized block graphics (each nibble +quadrupled into the cell); otherwise codes &80–&A8 index from (UDG) and +higher codes from (HUDG). + +#### PRCRLCDS +Control codes with operands (16–23) redirect the channel output address so +the next 1–2 "printed" bytes are collected as parameters (TVDATA), then +CCRP2 applies them: INK…OVER via PRCOITEM, AT with lower-screen growth +(scrolling the upper screen when they collide), TAB modulo the window width. + +#### SCRLSCR +On window-bottom CR: during AUTOLIST, hitting the bottom aborts the listing +via LISTSP. Otherwise, when SCRCT expires, "scroll?" is prompted on the +lower screen; N/space/BREAK raises error 14, anything else resets the count +and scrolls. Lower-screen scrolls that would collide with the upper print +position scroll the upper window too. + +--- + +## tapemn.asm + +| Entry point | Description | +|---|---| +| [SLMVC](#slmvc) | Common parser for SAVE/LOAD/MERGE/VERIFY: OVER flag, device letter override, filename, then the type qualifiers | +| `HDR5`/`HDR6`/`SVDS1` | Type qualifiers: SCREEN$ (mode+palette+line table), DATA a()/a$() (arrays), CODE start,len,exec / LINE n (auto-run) | +| [LVMMAIN / LKTH / LDFL](#lvmmain) | LOAD/VERIFY/MERGE execution: fetch headers until the name matches, then dispatch by type | +| `CDSCVE` | Verify any file / load CODE (with relative-page address fix-up and optional execute) | +| [LDPRDT / LDPROG](#ldprdt) | Load a program or array file: open room, load, then rebuild NVARS/NUMEND/SAVARS pointers from the header's three lengths, run auto-run line if present | +| `LDSCRN` | Load/verify SCREEN$ into an open (or newly opened) screen, restoring palette and line-interrupt table | +| `LDVDBLK`/`LDDBLK`/`LDVD2` | Load or verify one data block (tape via LDBLK, net, or DOS hook by device) | +| `SLVMC` | SAVE execution: write header then data block(s) | + +### Details + +#### SLMVC +Builds the 80-byte request header at HDR (layout documented at the top of +the file: type 16–20, 10-char name, flags, per-type info at offset 16, +start/length/execute triples at offset 31). `SAVE OVER` sets OVERF; a +leading device string ("T"/"D1:"/"N2:" style via PSLD/SLDEV) selects +tape/disc/net; DOS devices divert through hook codes. + +#### LVMMAIN +For tape: LDBLK reads each header, the name is compared (null name matches +anything, and names are echoed unless suppressed), then the type routines +check/allocate memory. Program loads wipe the current program first +(including CLEAR-like variable reset); "Loading error" (19) and type +mismatches guard each stage. DOS entry points LKTH/LDFL let the DOS reuse +the tail of the flow after loading headers/files itself. + +#### LDPRDT +Programs: room is checked against the header length, the block loaded over +the program area, then NVARS/NUMEND/SAVARS are reconstructed by adding the +three stored lengths to PROG, remaining pointers reset, RESTORE 0 done, and +the auto-run line (header offset 37: 0,lo,hi) GOTOs if present. Arrays: +any existing array of the stored name is deleted and the new record spliced +into SAVARS. + +--- + +## tapex.asm + +| Entry point | Description | +|---|---| +| [SABLK / SABYTES](#sablk) | Save a block of CDE bytes from HL to tape: leader (speed-compensated cycle count), sync, type byte, data with border stripes, parity | +| [LDBLK / LDBYTES](#ldblk) | Load/verify a tape block: leader detection, sync hunt, speed measurement (self-adjusting to the recorded TSPEED), bit slicing, parity check | +| [EDGEC](#edgec) | (tadjm.asm's EDGE2 partner) — double-edge timing used by LDBLK | +| `WNF`/`WNH`/`NIXJ` | Net save/load of a block with parity (station addressing via OTHER) | +| `NMOUT`/`NMIN` | Single byte out/in on the net/MIDI port with timing | +| `CKNET` | Wait for the net to fall quiet before transmitting | + +### Details + +#### SABLK +The SAM format is ZX-like but speed-programmable: PSLD's number sets the +half-cycle period; the leader cycle count is scaled so leaders last roughly +constant time at any speed. Data bytes stream from paged memory via the +same page-stepping used by FARLDIR. Type byte 01=header, FF=data. + +#### LDBLK +Measures the leader to calibrate thresholds (accepting a wide speed range, +including ZX-recorded 17-byte headers for LOAD compatibility), hunts the +sync pulse pair, then slices bits by comparing pulse lengths against the +measured midpoint. CY on success; BREAK and mis-timing abort with NC +("Loading error" upstream). + +--- + +## using.asm + +Despite the name, mostly memory-management and search plumbing (the USING$ +function itself was dropped — its token slot is unused). + +| Entry point | Description | +|---|---| +| [XOINTERS](#xointers) | The pointer-adjustment engine behind MAKEROOM/RECLAIM: shift the 14 (page,addr) system variables at/above the change point, plus FOR records and BASIC-stack frames | +| `SMBW`/`SMBS` | Point MEM at scratch (workspace-based) calculator memories | +| `SADJ` | Adjust BASIC-stack frame addresses after a move | +| `SNDA2` | Send a byte to the printer port (with BREAK polling and busy wait) | +| [HEAPROOM](#heaproom) | Reserve/release BC bytes of heap between HPST and the BASIC stack (public vector &0106) | +| [IMINSTR / INARRAYEN / MINSR](#iminstr) | INSTR([start,]s$,t$) — the search driver over paged strings using R0INST | +| [IMLENGTH](#imlength) | LENGTH(n,name) — element length/count/dims of a variable without evaluating it | +| [IMSTRINGS](#imstrings) | STRING$(n,a$) — n repetitions of a$ built in workspace | +| `CIFILSR` | CIRCLE/FILL coordinate unstacker (thin-pixel offset handling into TEMPW1) | +| `CRBBFN`/[CRTBF / CRTBFI](#crtbf) | Build runs of RLD/RRD (pixel roll) or LDI/LDD (byte move) opcodes in CDBUFF for the roll/scroll/GRAB engines | +| [DRTCRV / DRCURVE](#drtcrv) | DRAW x,y,angle — approximate the arc with chord segments computed on the FP stack | + +### Details + +#### XOINTERS +Called with the change location and size: every pointer sysvar from SAVARS +to PRPTR that points at or above the location is adjusted by the size (up +for MAKEROOM, down for RECLAIM), page bytes rippling as addresses cross 16K +boundaries; then every FOR-variable's looping address and every BASIC-stack +frame gets the same treatment (AFLPS/SADJ/ASSV in tadjm.asm do the walking). +It also computes PAGCOUNT/MODCOUNT — the number of bytes between the change +point and WKEND — for the FARLDIR/FARLDDR that follows. + +#### HEAPROOM +The heap grows up from HPST toward the down-growing BASIC stack; +BC +reserves, −BC releases (over-release just empties it). NC = insufficient +room with HL = shortfall. + +#### IMINSTR +Optional start position (clamped), target copied to INSTBUF (1–255 bytes), +then R0INST (misc2.asm) scans the search string across page boundaries in +16K chunks. Returns position or 0. + +#### IMLENGTH +LENGTH(0,a) = bytes per element, LENGTH(1,a()) = elements/dims etc.: looks +up the variable, then indexes its header without touching data — also +handles simple strings (length) and numbers (5). + +#### IMSTRINGS +Multiplies out n×len (error 42 past 65535), opens workspace, and LDIRs the +source repeatedly. + +#### CRTBF +Writes A copies of a 2-byte opcode (`ED A0` LDI etc., or RLD/RRD for pixel +shifts) into CDBUFF followed by RET — the ROM generates straight-line code +because a run of LDIs beats LDIR ~16%, and RLD runs have no loop form at +all. CDBUFF is called with (HL/DE/BC) set by the roll/scroll/GRAB/PUT/CLS +engines. + +#### DRTCRV +For DRAW x,y,a: computes the chord count and per-segment displacement from +the turn angle (in calculator code, memories 0–5), then issues that many +short DRAWs, accumulating rounding so the curve closes on the endpoint. + +--- + +## misc31.asm + +| Entry point | Description | +|---|---| +| [CALLER / CALLX path](#caller) | CALL addr[,params]: evaluate up to 15 parameters (numbers → FP stack order, strings → descriptors), then far-call the address | +| [SETUPVARS](#setupvars) | Create/refresh the ERROR, STAT and LINO variables before an ON ERROR handler runs | +| [MNINIT](#mninit) | Cold start: RAM sizing (256/512K detection into PRAMTP), ALLOCT init, system variables from CHIT/MAIT, screen 1 open, charset unpack, DEF KEY defaults, copyright banner via error &50 | +| [NEW / NEW2](#new) | NEW: close all screens except 1, free pages, reset RAMTOP and re-init (keeping FISCRNP…PRAMTP) | +| `RBOW…` | The power-on "rainbow" line-interrupt colour table | +| [UPACK](#upack) | Unpack the 5-bit-compressed character set to 8-byte matrices at CHARSVAL (plus the 13 descender patches) | +| `CLSHS`/`CLSHS2` | CLS # — full window/stream/colour reset | +| [COLOUR (PALETTE)](#colour-palette) | PALETTE i,c or i,b,c, optionally LINE l — set palette memories, flashing pairs, and the per-scan LINICOLS table | +| `FLITE`/`FLITD` | Find the line-interrupt table end/entry | +| `PALSW` | Save/restore palette entries 0–3 across mode 2 switches | + +### Details + +#### CALLER +Parameters after the address: numbers are pushed on the FP stack (count in +TEMPB3, passed in A to the called code), strings passed as stacked +descriptors. The call itself goes through PDPSUBR paging (0–512K address) +with IX saved; the routine returns to NEXTSTAT. + +#### SETUPVARS +Before entering an ON ERROR handler, three numeric variables are created or +updated: `error` (ERRNR), `stat` (SUBPPC), `lino` (PPC) — so handlers can +inspect the failure. + +#### MNINIT +Sizes RAM by writing markers at each page (detecting 256K mirrors), builds +ALLOCT (pages marked free/used/non-existent), initialises the system page, +streams, channels (from CHANTAB), windows, palette (INITCOLS + rainbow +LINICOLS), unpacks the charset, installs default DEF KEYs (DKSRC: F0=LIST, +F4=RUN, F9=BOOT…), then reports via the MGT banner (error &50) into the +main loop. + +#### NEW +Preserves only FISCRNP…PRAMTP (screen 1's page, allocation state, RAMTOP, +memory size), frees all other BASIC-owned pages and screens, and re-runs +most of MNINIT. + +#### UPACK +Each character is 7 five-bit slices packed across bytes; unpacking centres +the 6 significant pixels with 2/1 blank columns and zeroes the 8th scan, +then a patch list (U8TAB) fixes the 13 characters that need the bottom scan +(descenders, comma, semicolon…). + +#### COLOUR (PALETTE) +`PALETTE` alone clears LINICOLS and re-seeds both palette tables from +INITCOLS. With arguments it writes palette memory i (0–15) with colour c +(0–127) in *both* tables, or two colours b,c (one per table → flashing). +With `LINE l` the change goes into the LINICOLS table instead (4-byte +entries kept sorted by scan; up to 127; error 25 when full), taking effect +mid-frame via the line interrupt. + +--- + +## misc32.asm + +| Entry point | Description | +|---|---| +| [BEEP / BEEPP2](#beep) | BEEP duration,pitch: convert pitch (semitones, −60…69, fractions allowed) to period via octave halving, then the timed speaker loop | +| `ZAP`/`POW`/`BOOM`/`ZOOM` | Canned sound effects driving the sound chip / speaker | +| `BGRAPHICS` | BLOCKS n — block graphics on/off (BGFLG) | +| `KEY` | KEY position,code — repoke the 276-byte key map | +| `SLDEVICE` | DEVICE letter[speed/number] — set the default save/load device (PSLD) | +| [PAUSE](#pause) | PAUSE [n] — wait n frames (0 = forever) or until a key | +| [PRCOITEM2](#prcoitem2) | Execute a colour item: INK/PAPER/FLASH/BRIGHT/INVERSE/OVER parameter into the temporary variables, per-mode (attribute bits vs mode 2/3 bytes) | +| [BORDER / SETBORD](#border) | BORDER n: border port value, BORDCR, mode 2/3 lower-screen colours | +| `CVLSP`/`M3TO2` | Convert mode 3 colour bytes to non-striped mode 2 equivalents | +| [WINDOW](#window) | WINDOW [lhs,rhs,top,bot] — set (or reset) the upper window, clamped to WINDMAX | +| `OUT`/`STOP`/`RANDOMIZE` | OUT port,val; STOP (error 16); RANDOMIZE [n] (seed from FRAMES if 0) | + +### Details + +#### BEEP +Pitch \(p\): the octave \(\lfloor p/12 \rfloor\) offsets a base-frequency +table lookup; the fractional semitone interpolates. Produces DE=cycles−1, HL=half-period in +8T units for BEEPP2, which toggles bit 4 of the KEYPORT with interrupts off +(also the public vector &016F). + +#### PAUSE +Flushes the keyboard, HALTs per frame decrementing the count, exits on +key/BREAK. + +#### PRCOITEM2 +The run-time half of INK/PAPER/…: validates ranges per mode (mode 3 inks +0–15 map into paper/ink nibble pairs; INK 8/9 and PAPER 9 set the PFLAGT +match/contrast bits; mode 2 uses solid bytes), updating ATTRT/MASKT/PFLAGT +or M23PAPT/M23INKT. OVER 0–3 also sets GOVERT for PUT/plot. + +#### BORDER +Sets BORDCOL (hardware), BORDCR (modes 0/1 lower screen attribute) and +M23LSC (modes 2/3 lower screen colours), converting for mode 2 if active. + +#### WINDOW +No arguments restores the full-screen window; otherwise validates the +rectangle against WINDMAX and stores UW*: subsequent PRINT/CLS/scroll are +confined to it. + +--- + +## scrfn.asm + +| Entry point | Description | +|---|---| +| [COPY / JTCOPY / JGCOPY](#copy) | COPY (text screen dump) and COPY CHR$ (graphics dump) — both via the DMPV vector (no printer driver in ROM) | +| [IMSCREENS / IMSCSR](#imscreens) | SCREEN$(l,c): capture the character cell, normalise it, and match against the character set and UDGs | +| `SCREENSR` | The matcher: quick scan on bytes 3/4, full 8-byte check on candidates | +| [LSTR1 / LSTLNL](#lstr1) | LIST part 2: OUTLINE each line until past LAST (or the autolist window fills) | +| [OUTLINE](#outline) | Print one program line: number (5-column), `>` cursor, indent, then detokenize the text — the reference "tokens back to text" routine | +| `STENTS` | Scroll the LPT line-number table with the window | +| [SPACES / INCSPCS / DECSPCS](#spaces) | Pretty-listing indent engine driven by the structural tokens (DO/FOR/IF/DEF PROC increase, LOOP/NEXT/END… decrease) | +| [EDPTR2](#edptr2) | Print the edit/INPUT line to the lower screen, cursor placement, blanking of leftovers | +| `CUOPP`/`OPCURSOR`/`OPCUR2` | Cursor rendering: match KCUR against the line pointer during output; inverse-video cursor glyph via KURCHAR | +| `PRLCU`/`PRFLQUERY`/`PRINVERT` | The `>` line cursor, the flashing `?` syntax-error marker, inverse printing | +| [PRAREG / PRNUMB1 / PRNUMB2](#prareg) | Print integers: 1–3 digits (A), plain (BC), or padded to 5 with leading spaces (line numbers) | + +### Details + +#### COPY +Both flavours only check syntax and jump through DMPV — printer dump code is +expected to be installed (e.g. by a DOS or utility) at that vector. + +#### IMSCREENS +Reads the cell at (l,c) — via CHARCOMP for modes 2/3 (with 6-pixel column +rotation), direct copy for modes 0/1 (with inverse normalisation by the +top-left pixel) — then SCREENSR compares against the 96 ROM characters and +41 UDG codes, returning the character as a 1-byte string or "" if no match. + +#### LSTR1 +The listing loop: after each OUTLINE prints a CR, reads the next line number +(with ROM1 paged out) and stops past LAST; autolisting additionally stops +when the window is full. + +#### OUTLINE +The detokenizer — a program converting the stored form back to text follows +exactly this: print the big-endian line number right-aligned to 5 columns; +mark the LPT row; print `>` for EPPC or a space; consult LISTFLG/SPACES for +indent; then loop over the text with `RDCN` (read char, *skip &0E+5 forms +invisibly*), tracking INQUFG across &22 quotes so ":" inside strings isn't +treated as a separator; ":" outside strings (pretty mode) becomes +CR+6-space indent; every other byte goes to RST &10 where tprint.asm +expands tokens (&85–&FE and &FF-pairs) to keywords and prints ASCII as-is. +The XPTR position prints a flashing `?`. Ends at the CR. + +#### SPACES +Maintains NXTSPCS/CURSPCS (+THEN variants): DO, long IF, DEF PROC and FOR +add LISTFLG columns of indent from the next statement; LOOP, END PROC, +END IF and NEXT remove it from the current one; EXIT IF/LOOP IF/long ELSE +outdent just their own statement; short IF/ON indent only to end-of-line. + +#### EDPTR2 +The lower-screen editor display: prints the line via OUTLN25 (no line +number), tracks where the cursor lands (CUOPP swaps in when the screen +position matches KPOS), blanks any residue of the previous, longer +rendering, and preserves the "old position" so re-prints overlay exactly. + +#### PRAREG +`PRNUMB2` (listings) pads to 5 with spaces then digits via repeated +subtraction of 10000/1000/100/10; `PRNUMB1` suppresses padding; numbers +≥ &FF00 print as 0 (the edit line's PPC). + +--- + +## text.asm + +Pure data: + +| Region | Description | +|---|---| +| `UMVAL` | Utility messages 0–8 (banner, "scroll?", "Start tape…", "Basic:", array names) | +| `ERRMVAL` | Error messages 0–55, compressed with COMPLIST codes | +| `COMPLIST` | The 32 compression fragments (see [constants.md](constants.md#textasm)) | +| [KEYWTAB](#keywtab) | The keyword table: 196 words — immediate functions, FPC functions, binary operators, qualifiers &85–&8F, commands &90–&FE, and the trailing INK entry | +| `DKSRC` | Default DEF KEY definitions (F0=LIST, F1=RENUM:, F2=PRINT:, F3=MODE:, F4=RUN, F5=CONTINUE, F6=CLS #, F7/F8=LOAD ""/LOAD "" CODE, F9=BOOT) | +| `CHIT`/`MAIT` | System-variable initialisation blocks (18 + 26 bytes) | +| `CHANTAB` | Initial channel records: K, S, R (editor insert), P, $ (stream 16), B (printer byte) | +| `KSRC`/`CKTAB` | The 3-plane key map (normal/caps/symbol) and control-key table | +| `INITCOLS` | Initial palette (16 colours + 4-colour-mode set) | +| [CMDADT](#cmdadt) | The command address table: one word per token &90–&F6 (see [tokenized-program-format.md](tokenized-program-format.md)) | +| `U8TAB`/`SUBTAB` | Bottom-scan patch table for the charset; powers-of-ten for PRNUMB | +| `CHARSRC` | The 5-bit compressed character set (Simon N. Goodwin), ~600 bytes | + +### Details + +#### KEYWTAB +Words are stored with the last letter's bit 7 set; embedded spaces are +optional-match ("GO TO"). Order defines token values: match index 1–&49 → +function codes &3B–&83 (stored as &FF + code), &4A–&C4 → tokens &85–&FE +(index + &3B). The final entry "INK" maps to &FF and is transformed to the +PEN token (&A1) by the tokenizer. Unused slots hold "-". + +#### CMDADT +Indexed by (token − &90)×2, base published in the CMDADDRT sysvar. Bit 15 of +each address doubles as "routine is in ROM1" (addresses ≥ &8000 assemble in +the &C000-based ROM1, so the paging decision falls out of the address +itself). Unimplemented commands (DIR, FORMAT, ERASE, MOVE, RENAME, PROTECT, +HIDE…) point at NONSENSE and are expected to be intercepted by a DOS via +the CMDV vector. + +--- + +## romtest.asm + +Not part of the ROM. A small utility assembled separately: copies the live +ROM0/ROM1 and the freshly assembled image into paged RAM, compares them +byte-for-byte (with the volatile areas patched out), and prints the first +mismatch address in hex — used to verify that this source assembles to the +official v3.0 image. + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. diff --git a/docs/tokenized-program-format.md b/docs/tokenized-program-format.md new file mode 100644 index 0000000..fd643a4 --- /dev/null +++ b/docs/tokenized-program-format.md @@ -0,0 +1,518 @@ +# SAM BASIC — Tokenization, Compilation, and the In-Memory Program Format + +This document describes, from the ROM 3.0 source, how SAM BASIC turns a typed +line into its stored form, how that form is "compiled" (call-site address +caching) before execution, and — in enough detail to write a converter that +turns the stored bytes back into text — exactly what the final in-memory +token stream looks like. + +Source references: the tokenizer is `TOKPT2` in +[miscx2.asm](../miscx2.asm) (run from RAM via `TOKMAIN` in +[misc2.asm](../misc2.asm)); keyword matching is `GETTOKEN` in +[scrsel2.asm](../scrsel2.asm); the keyword table is `KEYWTAB` in +[text.asm](../text.asm); number embedding is `INSERT5B`/`CALC5BY` in +[eval.asm](../eval.asm); calling buffers are created by `MKCLBF`/`MAKESIX` +and resolved by `COMPILE`/`LKCALL`/`LOOKDF`/`LOOKDP` in [fn.asm](../fn.asm); +the reference detokenizer is `OUTLINE` in [scrfn.asm](../scrfn.asm) plus the +token printer in [tprint.asm](../tprint.asm). + +## Contents + +1. [Life cycle of a line](#1-life-cycle-of-a-line) +2. [The tokenizer](#2-the-tokenizer) +3. [Syntax check and the invisible 5-byte number forms](#3-syntax-check-and-the-invisible-5-byte-number-forms) +4. [String literals](#4-string-literals) +5. [FN and PROC calling buffers, and the COMPILE pass](#5-fn-and-proc-calling-buffers-and-the-compile-pass) +6. [Token rewriting: the two IFs and two ELSEs](#6-token-rewriting-the-two-ifs-and-two-elses) +7. [The final in-memory format (detokenizer specification)](#7-the-final-in-memory-format-detokenizer-specification) +8. [The complete token tables](#8-the-complete-token-tables) +9. [The 5-byte number format](#9-the-5-byte-number-format) +10. [Execution: how the stored form is consumed](#10-execution-how-the-stored-form-is-consumed) + +--- + +## 1. Life cycle of a line + +When ENTER is pressed in the editor, the main loop (`MAINELP`, +[mainlp.asm](../mainlp.asm)) processes the edit line (ELINE) in this order: + +```text +EDITOR collect keystrokes into ELINE (plain ASCII + any tokens that + arrived via EDIT/DEF KEY, which insert already-tokenized text) +TOKMAIN tokenize: replace spelled-out keywords with token bytes +LINESCAN syntax-check every statement; as a side effect the expression + evaluator INSERTS the invisible 5-byte forms after every numeric + literal, and FN/PROC references get 6-byte calling buffers +then either: +INSERTLN (line starts with a number) copy the line — tokens, 5-byte + forms, calling buffers and all — into the program area +or: +COMPILE + LINERUN (no line number) resolve calling buffers and execute +``` + +Two consequences worth stating up front: + +* **A stored program line is byte-for-byte what the syntax checker left in + ELINE.** Numbers carry both their ASCII text *and* a pre-converted 5-byte + binary value; FN/PROC calls carry both the name text *and* a 6-byte address + buffer. Nothing is recomputed from text at run time. +* Editing a line reverses the process: `EDIT` lists the line back into ELINE + through channel "R", which expands tokens to text, while the &0E forms are + simply *not printed* — they are recreated by the next LINESCAN. + +`VAL`/`VAL$` ([rom1fns.asm](../rom1fns.asm) `FPVAL`) run the identical +pipeline — `TOKDE` then a checking `SCANSR` pass (which inserts 5-byte forms +into the workspace copy) then an executing pass — which is why VAL accepts +full expressions including spelled-out keywords. + +`KEYIN` and `INPUT` also funnel their text through `TOKMAIN`+`LINESCAN` +(INPUT strips the inserted forms again with `REMOVEFP` before treating the +text as data). + +--- + +## 2. The tokenizer + +`TOKMAIN` copies the tokenizer body from ROM1 to `CDBUFF+&80` in the system +page and executes it there (ROM1 occupies the same addresses as the text +being tokenized, so it cannot run in place). The algorithm (`TOKPT2`): + +1. **Scan** the line from the start. Stop at CR (&0D). +2. Bytes that cannot start a keyword are skipped, with three special cases: + * `"` — skip to the closing quote (or CR). **Nothing inside string + literals is ever tokenized.** + * &FF — an already-present function token; skip the prefix *and* its code + byte (so re-tokenizing an edited line is safe). + * `<` and `>` are treated as possible word starts (for `<>`, `<=`, `>=`). +3. At a candidate word start (letter, `<`, `>`): copy up to **15 characters** + to a scratch buffer, then call **`GETTOKEN`** with the keyword table + (`KEYWTAB`, `KEYWNO` = 196 words). If GETTOKEN fails, the `MTOKV` vector + is offered the word (user-extensible tokenizers); if that also fails, the + scan resumes after the *whole* run of letters/underscores/`$` — so the + tail of `printer` is not matched as a keyword. +4. `GETTOKEN` (scrsel2.asm) matches case-insensitively against the list + (each list word's last letter has bit 7 set). Embedded spaces in list + words (`GO TO`, `DEF FN`, `END PROC`, `LOOP IF`, `ON ERROR`, …) are + *optional* in the input. After a complete match, the next input character + must fail `ALDU` (letter, `_`, or `$`) unless the list word ends in `=`, + `>` or `$` — this is the rule that leaves `printer`, `print_out` and + `print$` alone while tokenizing `print1` and `print:`. +5. The 1-based match index A becomes the stored form: + * **A < &4A** (the function section of the table): code = A + &3A, + giving &3B–&83. The **first letter of the word is overwritten with the + prefix &FF** and the code follows: functions and the alphabetic binary + operators are stored as the two bytes `FF cc`. + * **A ≥ &4A**: token = A + &3B, giving a **single byte &85–&FE**. + Special case: the final list entry `INK` would yield &FF; the tokenizer + substitutes **&A1 (PEN)** so INK may be typed as a synonym for PEN. +6. **Space absorption**: if the character before the word is a space, the + token overwrites that space (one leading space is absorbed); if the + character after the matched word is a space, it is included in the region + that is closed up (one trailing space absorbed). The remaining spelled-out + letters are removed with `RECLAIM1`. The lister re-synthesises these + spaces on output, so listings look unchanged. +7. If the token just placed was **REM (&B7)**, tokenization stops for the + rest of the line (REM text is preserved verbatim). Otherwise the scan + continues after the token. + +Note what the tokenizer does **not** do: it does not touch numbers, does not +create the &0E forms, and does not validate anything. All of that happens in +the syntax-check pass. + +--- + +## 3. Syntax check and the invisible 5-byte number forms + +`LINESCAN` runs the normal statement interpreter with FLAGS bit 7 = 0 +("checking"). Command routines validate argument shapes and abort before +doing work (`ABORTER`). Expressions go through the same evaluator as at run +time (`SCANSR`, eval.asm), and this is where literal optimization happens. + +**At check time**, when the evaluator meets a numeric literal (digits, `.`, +`&` hex, or the BIN token) — `INSERT5B`: + +1. `CALC5BY` parses the text and leaves the value on the calculator stack + (decimal with optional fraction and `E±nn`, `&`/`…H` hex via + `AMPERSAND`, or binary after BIN). +2. `MAKESIX` opens **6 bytes in the line immediately after the literal + text** and writes the marker **&0E**. +3. The value is popped off the calculator stack and its 5 bytes are copied + after the marker. + +So the stored form of `100` in a line is: + +```text +31 30 30 0E 00 00 64 00 00 +'1''0''0' ^ [ 5-byte value ] + number marker +``` + +**At run time** the evaluator does the reverse: on seeing a digit it scans +forward to the next &0E (`LK0ELP`) and LDIRs the 5 bytes straight onto the +calculator stack — the ASCII is never re-parsed. + +Everything else in the ROM knows the convention: + +* The `NUMBER`/`RDCN` routine in the RST area of main.asm adds 6 to the + pointer whenever it reads &0E — the canonical "skip invisible form" + primitive used by the searcher, the statement skipper, and the lister. +* `SKIPSTATS`, `FINDERS` (program search) and `READ LINE` all call `NUMBER` + so quoted colons, THEN, and number bytes that happen to look like &22 or + &0D never confuse them. +* `REMOVEFP` strips every `&0E+5` group from a region (INPUT lines, DEF + KEYCODE bodies) before the text is reused as text. +* **RENUM** (`CHGREF`, miscx1.asm) maintains the invariant: after renumbering + it writes the new value into the 5-byte form *and* re-prints the digits, + resizing the line if the digit count changed. + +Limits enforced here: statements per line ≤ 127, line text ≤ &3EFF bytes, +line numbers 1–65279 (&FEFF). + +--- + +## 4. String literals + +String literals are **stored as typed and used in place**. At run time +(`SQUOTE`, eval.asm) a quoted string with no embedded `""` pairs is *not +copied anywhere*: the evaluator stacks a 5-byte string descriptor +(page, start, length) pointing **into the BASIC line itself**. Only strings +containing doubled quotes are copied (with the escapes collapsed) into the +workspace at run time. + +So there is no separate stored form for strings — the "optimization" is the +absence of one, and any detokenizer simply copies the bytes between the +quotes verbatim (bytes ≥ &80 inside quotes are UDG/graphic characters, never +tokens; the tokenizer guaranteed that by skipping quoted text). + +--- + +## 5. FN and PROC calling buffers, and the COMPILE pass + +### Creation (syntax-check time) + +When the checker meets `FN name…` (`FNSYN`) or a statement beginning with a +letter — a procedure call (`PROCSY`) — it validates the name and argument +list, then `MKCLBF` opens **6 bytes immediately after the name**: + +```text +FN call: 0E FE FE FE ?? ?? +PROC call: 0E FD FD FD ?? ?? +``` + +The &0E leader makes every generic routine treat the buffer exactly like a +number form (skip 6). The FD/FE filler marks it as an *unresolved* FN/PROC +buffer. + +`DEF FN` itself (`DFNP2`, miscx2.asm) gets the same treatment for each +**parameter**: after each `letter` or `letter$` in its parameter list a +plain `0E xx xx xx xx xx` buffer is opened — at call time the argument +values are copied into these slots and `LKFNVAR` resolves single-letter +variables from them while the FN expression is evaluated. + +### Resolution — `COMPILE` (fn.asm) + +`COMPILE` runs before every execution: at `RUN`/`CLEAR`, and for the edit +line before every direct command. `COMPFLG` bit 7 (set by INSERTLN, LOAD, +DELETE, RENUM, KEYIN) requests a whole-program pass; the edit line is always +processed (FNs only when `REFFLG` recorded an FN in the line). + +The pass does three things: + +1. **Labels**: every `LABEL name` line assigns its own line number to the + numeric variable `name` (so `GOTO name` works via a variable). +2. **DEF FN table**: one scan collects page/address of every `DEF FN` into + INSTBUF (≤ 170 definitions, else error 52). +3. **Buffer patching**: `LKCALL` finds every calling buffer by searching for + the byte pattern + + ```text + (not &0E) &0E FDFD|FEFE (byte with bit 7 set) + ``` + + — a lone preceding &0E distinguishes a buffer from a numeric literal + whose 5 bytes happen to contain FD/FE pairs, and `0E FE FE 80…` can never + be a line header because it would imply a line length > 32768. The + call-site name is read *backwards* from the buffer (letters, digits, + `_`, `$`; a preceding `FF 42` "FN" token is skipped), then matched + (case-insensitively, spaces ignored) against the DEF FN table or against + `DEF PROC` statements found at line starts (`LKFC`). The buffer's last + three bytes are patched: + + ```text + resolved: 0E FE FE pp aa aa pp = page OR &80, aaaa = address + unresolved: 0E FE FE FF ?? ?? (bit 5 set = "no DEF FN/PROC") + ``` + + For FNs the address points just past the DEF FN name (at its `(` or + `=`); for PROCs it points at the DEF PROC line so parameters and the + body can be located. Page byte flags: bit 7 always set, bit 6 = external + command (XCMDP mechanism), bit 5 = missing (checked at call time: errors + 7 "FN without DEF FN" / 12 "Missing DEF PROC"). + +### Use (run time) + +`IMFN`/`PROCS` scan forward from the current position to the first &0E, +skip the two FD/FE bytes, page in `pp AND &1F`, and jump — no name search +ever happens during execution. Since programs move (edits, MAKEROOM) and +saved files are reloaded at different addresses, the addresses are simply +recomputed by the next COMPILE; the FD/FE signature bytes are what make the +buffers findable again. + +--- + +## 6. Token rewriting: the two IFs and two ELSEs + +The keyword list contains IF twice (&D7 "long IF", &D8 "short IF") and ELSE +twice (&D9 "long ELSE", &DA "short ELSE"); the tokenizer always produces the +*first* occurrence (&D7 / &D9). The **syntax checker rewrites the byte in +the stored line**: + +* `LIF` (&D7) becomes `SIF` (&D8) when the condition is followed by THEN + (`LIF`/`SIF` in do.asm write `LD (HL),&D8` into the line). +* `LELSE` (&D9) becomes `ELSE` (&DA) when the preceding IF on the line was + short; `LELSE LIF cond` becomes `LELSE SIF cond` (the ELSE IF chain form). + +Both pairs list identically ("IF", "ELSE"), so the distinction is invisible +in listings but present in the stored bytes — a detokenizer must map both +codes of each pair to the same word. + +--- + +## 7. The final in-memory format (detokenizer specification) + +### Program area + +The program starts at (PROGP)/(PROG) and is a sequence of lines in ascending +line-number order, terminated by a byte **&FF** where the next line-number +MSB would be. (The edit line, at (ELINE), has the same *text* format but no +4-byte header — it is `[optional digits][text] 0D FF`.) + +### Line layout + +```text +offset 0 line number, MSB FIRST (1–&FEFF; &FF here = end of program) +offset 1 line number, LSB +offset 2 text length, LSB (length of text INCLUDING the final 0D) +offset 3 text length, MSB +offset 4 text bytes … +last 0D +``` + +Note the mixed endianness: line number big-endian (so lines compare +MSB-first during search), length little-endian. + +### Text bytes + +Process the text sequentially with an *in-quotes* flag (initially clear): + +| Byte(s) | Meaning / action for a converter | +|---|---| +| &0D | End of line. | +| &0E | Invisible form — **emit nothing, skip 6 bytes total** (the marker + 5). This covers numeric literals (whose ASCII text precedes them), FN/PROC calling buffers (`0E FE FE …`/`0E FD FD …`, whose name precedes them), and DEF FN parameter buffers. Never toggles the quote flag, never counts as a statement boundary. | +| &22 `"` | Emit, and toggle the in-quotes flag. (A doubled `""` inside a string is simply two quote bytes: the flag toggles twice.) | +| &20–&7F | Plain ASCII — emit as-is. | +| &00–&1F (except &0D, &0E) | Literal control codes (colour controls &10–&15 with a parameter byte, AT &16 / TAB &17 with parameters, etc., typically inside strings or PRINT items). The interpreter skips them when scanning (RST &18 passes over bytes < &21); the lister sends them to the print routine, where they act. A text converter should pass them through (or escape them). | +| &80–&84 | Always UDG/block-graphic characters — emit as character codes, never keywords. | +| &85–&FE, in quotes | UDG/graphic characters — emit as character codes. | +| &85–&FE, not in quotes | **Keyword token** — emit the keyword text from [the table below](#8-the-complete-token-tables), with the spacing rules given there. | +| &FF, in quotes | UDG/graphic character &FF. | +| &FF, not in quotes | **Function prefix**: the next byte (&3B–&83) selects a function or alphabetic operator from the second table; emit its text with its spacing rules. (Codes outside &3B–&83 after &FF do not occur in checked lines.) | + +Statement separators are the literal `:` byte (outside quotes) and the THEN +token (&8D); a converter need not treat them specially beyond normal token +expansion, except to reproduce SAM's pretty-listing (LIST FORMAT) behaviour, +which turns statement-separating `:` into newline + 6-space indent and +applies block indenting driven by the structural tokens (see `SPACES`, +scrfn.asm). + +### Spacing on output + +The tokenizer absorbed up to one space on each side of every keyword, so a +faithful lister re-inserts them (`POFN`/`POBTL`/`POMSG3` in tprint.asm): + +* Single-byte tokens (&85–&FE): print a **leading space** unless the + previously emitted character was already a space, and a **trailing + space** if the keyword ends in a letter or `$`. +* &FF-pair operators MOD, DIV, BOR, BAND, OR, AND (&7A–&80): leading and + trailing space. +* &FF-pair `<>`, `<=`, `>=` (&81–&83): no spaces. +* &FF-pair FN (&42) and BIN (&43): trailing space only. +* All other &FF-pair functions: no leading space; trailing space if the + name ends in a letter or `$` (i.e. all of them — so `SIN x`, `CHR$ a`). + +A converter that only needs *re-tokenizable* text (rather than +column-identical listings) can simply emit one space either side of every +keyword. + +### Worked example + +`10 IF a>1 THEN PRINT "ok": GO TO 100` is stored as (hex): + +```text +00 0A line number 10 (MSB first) +1C 00 text length 28 (includes the 0D) +D8 SIF ("IF" rewritten because THEN follows) +61 'a' +3E '>' (single-char operators stay plain ASCII) +31 0E 00 00 01 00 00 '1' + invisible form (value 1) +8D THEN +BB PRINT +22 6F 6B 22 '"' 'o' 'k' '"' +3A ':' +B4 GO TO +31 30 30 0E 00 00 64 00 00 '1''0''0' + invisible form (value 100) +0D +``` + +(The `>` comparison is a plain ASCII byte; only the two-character +comparisons and the alphabetic operators are FF-pairs.) + +--- + +## 8. The complete token tables + +### Single-byte tokens &85–&FE (and the &FF prefix) + +Derived from `KEYWTAB` order and the `CMDADT` comments in +[text.asm](../text.asm). "—" entries are unused codes (never produced by the +tokenizer; if encountered, treat as UDG characters). + +| Code | Keyword | Code | Keyword | Code | Keyword | Code | Keyword | +|---|---|---|---|---|---|---|---| +| &85 | USING | &A4 | BRIGHT | &C3 | DRAW | &E2 | DPOKE | +| &86 | WRITE | &A5 | INVERSE | &C4 | DEFAULT | &E3 | RENAME | +| &87 | AT | &A6 | OVER | &C5 | DIM | &E4 | CALL | +| &88 | TAB | &A7 | FATPIX | &C6 | INPUT | &E5 | ROLL | +| &89 | OFF | &A8 | CSIZE | &C7 | RANDOMIZE | &E6 | SCROLL | +| &8A | WHILE | &A9 | BLOCKS | &C8 | DEF FN | &E7 | SCREEN | +| &8B | UNTIL | &AA | MODE | &C9 | DEF KEYCODE | &E8 | DISPLAY | +| &8C | LINE | &AB | GRAB | &CA | DEF PROC | &E9 | BOOT | +| &8D | THEN | &AC | PUT | &CB | END PROC | &EA | LABEL | +| &8E | TO | &AD | BEEP | &CC | RENUM | &EB | FILL | +| &8F | STEP | &AE | SOUND | &CD | DELETE | &EC | WINDOW | +| &90 | DIR | &AF | NEW | &CE | REF | &ED | AUTO | +| &91 | FORMAT | &B0 | RUN | &CF | COPY | &EE | POP | +| &92 | ERASE | &B1 | STOP | &D0 | — | &EF | RECORD | +| &93 | MOVE | &B2 | CONTINUE | &D1 | KEYIN | &F0 | DEVICE | +| &94 | SAVE | &B3 | CLEAR | &D2 | LOCAL | &F1 | PROTECT | +| &95 | LOAD | &B4 | GO TO | &D3 | LOOP IF | &F2 | HIDE | +| &96 | MERGE | &B5 | GO SUB | &D4 | DO | &F3 | ZAP | +| &97 | VERIFY | &B6 | RETURN | &D5 | LOOP | &F4 | POW | +| &98 | OPEN | &B7 | REM | &D6 | EXIT IF | &F5 | BOOM | +| &99 | CLOSE | &B8 | READ | &D7 | IF (long) | &F6 | ZOOM | +| &9A | CIRCLE | &B9 | DATA | &D8 | IF (short) | &F7–&FE | — | +| &9B | PLOT | &BA | RESTORE | &D9 | ELSE (long) | &FF | *function prefix* | +| &9C | LET | &BB | PRINT | &DA | ELSE (short) | | | +| &9D | BLITZ | &BC | LPRINT | &DB | END IF | | | +| &9E | BORDER | &BD | LIST | &DC | KEY | | | +| &9F | CLS | &BE | LLIST | &DD | ON ERROR | | | +| &A0 | PALETTE | &BF | DUMP | &DE | ON | | | +| &A1 | PEN | &C0 | FOR | &DF | GET | | | +| &A2 | PAPER | &C1 | NEXT | | | | | +| &A3 | FLASH | &C2 | PAUSE | | | | | + +Notes: &D7/&D8 both list as `IF`; &D9/&DA both list as `ELSE`. Typing `INK` +tokenizes to &A1 (PEN). The tokenizer never emits &90–&93, &E3, &F1, &F2 as +*executable* commands' targets in this ROM (their CMDADT entries are +NONSENSE, reserved for DOS), but the tokens themselves are produced and +stored normally. + +### &FF-prefixed function/operator codes &3B–&83 + +| Code | Name | Code | Name | Code | Name | Code | Name | +|---|---|---|---|---|---|---|---| +| &3B | PI | &4E | — (CHAR$) | &61 | PEEK | &74 | USR$ | +| &3C | RND | &4F | PATH$ | &62 | DPEEK | &75 | — (INKEY$ dup) | +| &3D | POINT | &50 | STRING$ | &63 | DVAR | &76 | NOT | +| &3E | FREE | &51 | — (USING$) | &64 | SVAR | &77–&79 | — | +| &3F | LENGTH | &52 | — (SHIFT$) | &65 | BUTTON | &7A | MOD | +| &40 | ITEM | &53 | SIN | &66 | EOF | &7B | DIV | +| &41 | ATTR | &54 | COS | &67 | PTR | &7C | BOR | +| &42 | FN | &55 | TAN | &68 | — | &7D | — (BXOR) | +| &43 | BIN | &56 | ASN | &69 | UDG | &7E | BAND | +| &44 | XMOUSE | &57 | ACS | &6A | — | &7F | OR | +| &45 | YMOUSE | &58 | ATN | &6B | LEN | &80 | AND | +| &46 | XPEN | &59 | LN | &6C | CODE | &81 | <> | +| &47 | YPEN | &5A | EXP | &6D | VAL$ | &82 | <= | +| &48 | RAMTOP | &5B | ABS | &6E | VAL | &83 | >= | +| &49 | — (INARRAY) | &5C | SGN | &6F | TRUNC$ | | | +| &4A | INSTR | &5D | SQR | &70 | CHR$ | | | +| &4B | INKEY$ | &5E | INT | &71 | STR$ | | | +| &4C | SCREEN$ | &5F | USR | &72 | BIN$ | | | +| &4D | MEM$ | &60 | IN | &73 | HEX$ | | | + +Internally the evaluator subtracts &1A from these codes (giving &21–&69) to +index its dispatch tables — that offset appears throughout eval.asm and +fpcmain.asm but never in the stored program. + +--- + +## 9. The 5-byte number format + +The 5 bytes after an &0E marker (and every entry on the calculator stack) +use the same two-form representation as the ZX Spectrum: + +**Small integer form** (any integer −65535…65535): + +| Byte | Contents | +|---|---| +| 0 | &00 (marks integer form) | +| 1 | Sign — &00 positive, &FF negative | +| 2 | Value LSB (two's-complement 16-bit value; e.g. −5 is stored `00 FF FB FF 00`) | +| 3 | Value MSB | +| 4 | &00 | + +**Floating-point form** — the magnitude is \(m \times 2^{e-128}\) with the +mantissa normalised to \(0.5 \le m < 1\): + +| Byte | Contents | +|---|---| +| 0 | Exponent \(e\) (&01–&FF, bias &80) | +| 1 | Mantissa byte 1 — bit 7 **replaced** by the sign (0 = +, 1 = −); the mantissa's own top bit is implicit (always 1) | +| 2–4 | Mantissa bytes 2–4, most significant first | + +Byte 0 = 0 always means integer form; a zero *value* is +`00 00 00 00 00`. Examples: `1.0` FP form = `81 00 00 00 00` +(usually stored as integer `00 00 01 00 00`); `0.5` = `80 00 00 00 00`; +`π/2` = `81 49 0F DA A2`. + +A detokenizer normally ignores these bytes (the ASCII text precedes them), +but a *verifier* can decode and compare; conversely a program **generator** +must supply them, since the run-time evaluator reads only the 5-byte form. +Conversion between forms is `RESTACK`/`FPFORM` (mult.asm) and +`INT`/`FPTOBC` (tadjm.asm). + +--- + +## 10. Execution: how the stored form is consumed + +For completeness, the run-time reading conventions a converter should be +aware of: + +* **Character fetch** (`RST &18`/`RST &20`): skips every byte &00–&20 except + CR. Spaces and stray control bytes are therefore insignificant outside + strings — but note the tokenizer already removed at most one space around + keywords; others remain stored. +* **Command dispatch** (`STMTLP`): the first significant byte of a statement + must be ≥ &90 (a command token) or a letter (implicit PROC call — also + LET-less assignment is *not* supported; `LET` has its own token). The + token −&90, doubled, indexes the CMDADDRT word table. +* **Numbers**: first digit/`.`/`&`/BIN encountered in an expression triggers + the scan-to-&0E described in §3. +* **FN/PROC**: scan-to-&0E from the name, then use the patched page/address + (§5). +* **Statement/line addressing**: GOTO/GOSUB/RETURN/NEXT/LOOP store and use + (page, line-start address, statement-number) triples — statement numbers + count `:`/THEN boundaries from 1, so a converter that renumbers or edits + statements changes program meaning for CONTINUE/POP but not for stored + programs (those triples exist only on the run-time BASIC stack, never in + the program area). +* The only bytes in the *program area* that encode addresses are the + calling buffers — and they are recomputed by COMPILE before every run, so + a converter may safely emit them unresolved (`0E FE FE FE 00 00` / + `0E FD FD FD 00 00`) as the syntax checker does. + +> [!WARNING] AI Generated Documentation +> +> These docs were generated by @spectecjr using AI. They may contain errors, +> but appear to be correct. From f91224d099264644514e1cdc353fbe76c8c4871e Mon Sep 17 00:00:00 2001 From: Simon Cooke Date: Sun, 9 Aug 2026 03:51:19 -0700 Subject: [PATCH 2/5] Fixed up LaTex / KaTeX equations Needed to match GitHub conventions (straight LaTeX using $ / $$) --- docs/file-formats.md | 6 +++--- docs/machine-code-interface.md | 10 +++++----- docs/source-files.md | 30 +++++++++++++++--------------- docs/tokenized-program-format.md | 6 +++--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/file-formats.md b/docs/file-formats.md index 9f38871..6bcaa5a 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -25,8 +25,8 @@ back on load (the DOS re-enters the ROM's common code at `LDFL`/`LKTH`/ ## The 80-byte header Multi-byte *lengths and addresses* use the ROM's **page form**: three bytes -`pages, lo, hi` meaning \(\text{pages} \times 16384 + (\text{hi:lo} \bmod -16384)\) — the hi byte usually has bit 7 set (&8000-based section-C +`pages, lo, hi` meaning $ \text{pages} \times 16384 + (\text{hi:lo} \bmod +16384) $ — the hi byte usually has bit 7 set (&8000-based section-C address); loaders must mask bits 6–7 of the hi byte when forming the 14-bit remainder, and treat the whole triple as invalid ("none") when the first byte is &FF. @@ -57,7 +57,7 @@ byte is &FF. ## What the data block contains for a BASIC program (type 16) SAVE writes **one contiguous block** starting at `PROG` with length -\((\text{ELINE} - 1) - \text{PROG}\) — that is, everything from the first +$ (\text{ELINE} - 1) - \text{PROG} $ — that is, everything from the first program line up to *but excluding* the &FF terminator that ends the string/array area (the terminator is deliberately not saved; LOAD re-plants one). The block therefore contains, in order: diff --git a/docs/machine-code-interface.md b/docs/machine-code-interface.md index b61f81c..b553fcf 100644 --- a/docs/machine-code-interface.md +++ b/docs/machine-code-interface.md @@ -96,7 +96,7 @@ pages ROM1 in for the duration, and restores everything on exit). * Number forms (details in [tokenized-program-format.md §9](tokenized-program-format.md#9-the-5-byte-number-format)): small integer `00 sign lo hi 00`, or FP `exponent m1 m2 m3 m4` where the - magnitude is \(m \times 2^{e-128}\) with \(0.5 \le m < 1\), the mantissa's + magnitude is $ m \times 2^{e-128} $ with $ 0.5 \le m < 1 $, the mantissa's leading 1 implicit, and its bit 7 replaced by the sign. * Strings on the same stack are 5-byte descriptors: `page+flags, start-lo, start-hi, len-lo, len-hi`. @@ -116,7 +116,7 @@ Alternatively copy 5 bytes to (`STKEND`) yourself and add 5 to `STKEND`. ### Running a calculation -Example — hypotenuse \(\sqrt{x^2+y^2}\) of the two numbers already on the +Example — hypotenuse $ \sqrt{x^2+y^2} $ of the two numbers already on the stack (x below y): ```z80 @@ -173,16 +173,16 @@ the same 0–524287 address scheme used by PEEK/DPEEK/POKE. The value is interpreted **relative to the context base page** (page 0 for a normal BASIC program), and the ROM pages the target in for you: -| Address argument \(N\) | Executes at | Paging while your code runs | +| Address argument $ N $ | Executes at | Paging while your code runs | |---|---|---| | 0 – 16383 | &0000 + N | ROM0 (section A) — i.e. you can CALL ROM0 routines directly | | 16384 – 32767 | &4000 + (N − 16384) | The base (system) page, section B | | 32768 – 49151 | &8000 + (N − 32768) | Page base+1 selected into section C | | 49152 – 65535 | &C000 + (N − 49152) | URPORT = base+1, so the code runs in base+2 via section D | -| ≥ 65536 | &8000 + (N mod 16384) | Page base + \(\lfloor N/16384 \rfloor\) − 1 selected into section C | +| ≥ 65536 | &8000 + (N mod 16384) | Page base + $ \lfloor N/16384 \rfloor $ − 1 selected into section C | (That last row is why the ReadMe's `CALL 229385` reaches offset 9 of RAM -page 13 on a 256K machine: \(229385 = 14 \times 16384 + 9\).) +page 13 on a 256K machine: $ 229385 = 14 \times 16384 + 9 $.) **Register state on entry to your routine** (`CALLX`): diff --git a/docs/source-files.md b/docs/source-files.md index 1be60f6..96f98bd 100644 --- a/docs/source-files.md +++ b/docs/source-files.md @@ -456,7 +456,7 @@ pattern data then attributes; mode 0 has its own scan-stepping loop follows the text. #### CALCPIX -\(\text{scans} = \text{rows} \times \text{height}\) where the height comes +$ \text{scans} = \text{rows} \times \text{height} $ where the height comes from CSIZE (6–32), plus DHADJ (8) when the bottom half of a double-height character is being placed. @@ -1087,7 +1087,7 @@ the end unless it started off-screen. Updates XCOORD/YCOORD to the endpoint. `PLOTFD` fiddles coordinates through XOS/YOS/XRG/YRG (Y ends 0 at top), updates YCOORD/XCOORD, selects the screen page, and jumps through IY. Thin plot computes -\(\text{addr} = \&8000 + Y \times 128 + \lfloor X/4 \rfloor\) +$ \text{addr} = \&8000 + Y \times 128 + \lfloor X/4 \rfloor$ and rotates a 2-bit mask into place, honouring OVER and INVERSE against M23INKT/M23PAPT. @@ -1245,7 +1245,7 @@ At check time just validates subscript/slicer syntax. At run time: simple strings convert the pages+mod-16K length to a 16-bit length and stack a descriptor (bit 7 of page set = replace-on-assign); arrays walk the dimension list computing -\(\text{total} = (\dots(s_1 \times d_2 + s_2) \times d_3 + \dots) + s_n\), +$ \text{total} = (\dots(s_1 \times d_2 + s_2) \times d_3 + \dots) + s_n $, finally multiplied by 5 (numeric) or by the last dimension (string) and added to the data start. String results may then be sliced: `SLICING` handles `(a TO b)` with defaults, empty result for reversed @@ -1523,10 +1523,10 @@ DEVICE selects SPOSNU/SPOSNL/PRPOSN; returns D=row, E=col, A=RHS limit (WINDRHS or PRRHS), CY for printer. #### ANYDEADDR -\(\text{addr} = \&8000 + \text{scans} \times w + \text{col} \times b\), -with the per-mode scan width \(w\) and bytes-per-column \(b\): +$ \text{addr} = \&8000 + \text{scans} \times w + \text{col} \times b $, +with the per-mode scan width $ w $ and bytes-per-column $ b $: -| Mode | \(w\) (bytes/scan) | \(b\) (bytes/column) | +| Mode | $ w $ (bytes/scan) | $ b $ (bytes/column) | |---|---|---| | 0 | 32 (interleaved thirds layout) | 1 | | 1 | 32 | 1 | @@ -1732,12 +1732,12 @@ Chebyshev-based transcendental functions, all written as calculator code. |---|---| | `FPSIN`/`FPCOS`/`FPTAN` | Sine (W.E. Thomson's faster series), cosine = sin(x+π/2), tan = sin/cos | | `FPREDARG` | Reduce an angle to the −π…π range (V = x/2π fractional part scaled) | -| `FPEXP`/`FPPOWR2` | \(e^x = 2^{x \log_2 e}\); \(2^y\) with integer/fraction split (error 28 on overflow, 0 on deep underflow) | -| `FPPOWER` | \(N_1^{N_2}\): special-cases \(N_1 = 0\) and integer powers 0–&3F (by repeated multiplication), else \(e^{N_2 \ln N_1}\) | +| `FPEXP`/`FPPOWR2` | $ e^x = 2^{x \log_2 e} $; $ 2^y $ with integer/fraction split (error 28 on overflow, 0 on deep underflow) | +| `FPPOWER` | $ N_1^{N_2} $: special-cases $ N_1 = 0 $ and integer powers 0–&3F (by repeated multiplication), else $ e^{N_2 \ln N_1} $ | | `FPLOGN` | Natural log: exponent extraction + series on the mantissa | -| `FPARCTAN`/`FPARCSIN`/`FPARCCOS` | ATN (range-folded series); \(\operatorname{ASN} x = \operatorname{ATN}\dfrac{x}{\sqrt{1-x^2}}\); \(\operatorname{ACS} x = \pi/2 - \operatorname{ASN} x\) | +| `FPARCTAN`/`FPARCSIN`/`FPARCCOS` | ATN (range-folded series); $ \operatorname{ASN} x = \operatorname{ATN}\dfrac{x}{\sqrt{1-x^2}} $; $ \operatorname{ACS} x = \pi/2 - \operatorname{ASN} x $ | | [SERIES](#series) | The Chebyshev series engine: 12-coefficient loop driven by DECB with inline literals | -| `FPSQR` | (in rom1fns/transend flow) \(\sqrt{x} = x^{0.5}\) via the POWER path with a fast exponent halving | +| `FPSQR` | (in rom1fns/transend flow) $ \sqrt{x} = x^{0.5} $ via the POWER path with a fast exponent halving | ### Details @@ -1790,8 +1790,8 @@ cancellation case giving zero. #### DFPFORM `FPFORM` (= calculator op RESTACK) rewrites a small-integer entry -`00 sign lo hi 00` as exponent/mantissa form (exponent = \(\&90 - s\) where -\(s\) is the normalising shift; mantissa = the value shifted so bit 30 is +`00 sign lo hi 00` as exponent/mantissa form (exponent = $ \&90 - s $ where +$ s $ is the normalising shift; mantissa = the value shifted so bit 30 is the top set bit; sign in mantissa bit 7). `DFPFORM` does both operands for the multiply/divide/power paths. @@ -1802,7 +1802,7 @@ the top set bit; sign in mantissa bit 7). | Entry point | Description | |---|---| | [FPVAL / FPVALS](#fpval) | VAL/VAL$: tokenize the argument text in workspace, syntax-check it as an expression, then evaluate it (with FLAGS run bit borrowed) | -| `IMRND` | RND [(n)]: congruential seed update \(\text{seed} \leftarrow ((\text{seed}+1) \times 75 \bmod 65537) - 1\); returns a fraction, or an integer in \(0 \dots n-1\) | +| `IMRND` | RND [(n)]: congruential seed update $ \text{seed} \leftarrow ((\text{seed}+1) \times 75 \bmod 65537) - 1 $; returns a fraction, or an integer in $ 0 \dots n-1 $ | | `IMATTR`/`IMPOINT` | ATTR(l,c) and POINT(x,y) — read attribute byte / pixel ink number in any mode | | `GETCP` | Validate a line/col pair against limits | | `FPINKEY`/`FPINKEN` | INKEY$ [#n]: stream version reads the channel; builds a 1-char string | @@ -1958,7 +1958,7 @@ otherwise), and jump to the DOS init at &8009. | [PRFPBUF](#prfpbuf) | Raw conversion: integer part by repeated decimal subtraction/BCD, fraction by repeated ×10 of the binary fraction | | `DECDIGP`/`DECDIGN` | log10 bounds for a power of two (digits before point / zeros after) | | `DECIMIZE`/`PRBCD` | Binary→BCD conversion of the integer part and BCD→ASCII output | -| [POFTEN](#poften) | Multiply the FP top by \(10^{A}\) (A signed) — used by E-format input and output | +| [POFTEN](#poften) | Multiply the FP top by $ 10^{A} $ (A signed) — used by E-format input and output | ### Details @@ -2257,7 +2257,7 @@ mid-frame via the line interrupt. ### Details #### BEEP -Pitch \(p\): the octave \(\lfloor p/12 \rfloor\) offsets a base-frequency +Pitch $ p $: the octave $ \lfloor p/12 \rfloor $ offsets a base-frequency table lookup; the fractional semitone interpolates. Produces DE=cycles−1, HL=half-period in 8T units for BEEPP2, which toggles bit 4 of the KEYPORT with interrupts off (also the public vector &016F). diff --git a/docs/tokenized-program-format.md b/docs/tokenized-program-format.md index fd643a4..03e87df 100644 --- a/docs/tokenized-program-format.md +++ b/docs/tokenized-program-format.md @@ -462,12 +462,12 @@ use the same two-form representation as the ZX Spectrum: | 3 | Value MSB | | 4 | &00 | -**Floating-point form** — the magnitude is \(m \times 2^{e-128}\) with the -mantissa normalised to \(0.5 \le m < 1\): +**Floating-point form** — the magnitude is $ m \times 2^{e-128} $ with the +mantissa normalised to $ 0.5 \le m < 1 $: | Byte | Contents | |---|---| -| 0 | Exponent \(e\) (&01–&FF, bias &80) | +| 0 | Exponent $ e $ (&01–&FF, bias &80) | | 1 | Mantissa byte 1 — bit 7 **replaced** by the sign (0 = +, 1 = −); the mantissa's own top bit is implicit (always 1) | | 2–4 | Mantissa bytes 2–4, most significant first | From 3da9bc837bf87a6d1b81af9b1c7fe10087c9dccb Mon Sep 17 00:00:00 2001 From: Simon Cooke Date: Mon, 10 Aug 2026 16:34:01 -0700 Subject: [PATCH 3/5] Add annotated ROM source, user manual, and guides Three bodies of documentation, all derived from the ROM source itself rather than from the original printed manuals. annotated/ -- a parallel copy of the ROM source documented as a modern codebase would be: file-level overviews, per-routine contracts (entry, exit, registers, notes), inline explanation of non-obvious code, and named constants in place of magic numbers. All 39 source files, plus a new equates.asm. pyz80 builds the tree to an image byte-identical to the one built from the original source; verify_annotated.py checks equivalence from the text alone, which pinpoints which line differs when a build fails. docs/user-manual/ -- a 21-part manual taking a reader from their first PRINT to writing extensions: 15 chapters, an A-Z reference covering every keyword and function, and appendices for the 56 error reports, the character set, the token map and the operator priorities. Chapter 14 tabulates every system variable reachable through SVAR, marked for whether it is safe to write. docs/extending-basic.md -- the MTOKV/CMDV/RST28V/PRTOKV hooks with their exact entry and exit contracts, the (small) budget of free tokens, where extension code has to live and how to carve room for it, and four worked examples that assemble with pyz80. Also adds font-rendering.md and hudg.md, and cross-references the new documents from constants.md, memory-map.md and source-files.md. Several things widely believed about SAM BASIC turn out not to hold for ROM 3.0. The docs say so, with the evidence: * LET is mandatory. A bare name starting a statement is parsed as a procedure call, so "x = 1" gives Missing DEF PROC. * MOD and DIV bind at priority 14 -- tighter than * and /. * INK and PEN share one token, so INK 3 lists back as PEN 3. * The ROM has no external-command mechanism. Nothing reads XCMDP, and nothing sets the call buffer's "external" bit -- but the original comments show what was intended, and extending-basic.md reconstructs the design and finishes it. Written with AI assistance and not tested on hardware or in an emulator; every document carries that warning. --- annotated/README.md | 238 ++ annotated/assign.asm | 1172 +++++++++ annotated/check.sh | 145 ++ annotated/do.asm | 976 ++++++++ annotated/editor.asm | 858 +++++++ annotated/endprint.asm | 1160 +++++++++ annotated/equates.asm | 670 +++++ annotated/eval.asm | 1067 ++++++++ annotated/fn.asm | 1146 +++++++++ annotated/fpcmain.asm | 844 +++++++ annotated/grabput.asm | 567 +++++ annotated/graph0.asm | 642 +++++ annotated/graph1.asm | 380 +++ annotated/graph2.asm | 1333 ++++++++++ annotated/list.asm | 801 ++++++ annotated/lookvar.asm | 410 +++ annotated/main.asm | 872 +++++++ annotated/mainlp.asm | 925 +++++++ annotated/misc1.asm | 662 +++++ annotated/misc2.asm | 1137 +++++++++ annotated/misc31.asm | 812 ++++++ annotated/misc32.asm | 811 ++++++ annotated/miscx1.asm | 923 +++++++ annotated/miscx2.asm | 589 +++++ annotated/mult.asm | 987 ++++++++ annotated/nparpro.asm | 798 ++++++ annotated/printfp.asm | 682 +++++ annotated/roll.asm | 1140 +++++++++ annotated/rom1fns.asm | 1193 +++++++++ annotated/romtest.asm | 170 ++ annotated/samrom.asm | 73 + annotated/scrfn.asm | 918 +++++++ annotated/scrsel1.asm | 1125 +++++++++ annotated/scrsel2.asm | 815 ++++++ annotated/tadjm.asm | 1134 +++++++++ annotated/tapemn.asm | 1134 +++++++++ annotated/tapex.asm | 720 ++++++ annotated/text.asm | 1589 ++++++++++++ annotated/tprint.asm | 1018 ++++++++ annotated/transend.asm | 521 ++++ annotated/using.asm | 997 ++++++++ annotated/vars.asm | 655 +++++ annotated/verify_annotated.py | 357 +++ docs/constants.md | 16 +- docs/extending-basic.md | 2203 +++++++++++++++++ docs/font-rendering.md | 182 ++ docs/hudg.md | 248 ++ docs/memory-map.md | 7 +- docs/source-files.md | 9 +- docs/user-manual/01-first-steps.md | 205 ++ docs/user-manual/02-the-editor.md | 274 ++ .../03-numbers-strings-and-variables.md | 362 +++ .../04-expressions-and-operators.md | 326 +++ docs/user-manual/05-decisions-and-loops.md | 349 +++ .../06-procedures-and-functions.md | 340 +++ docs/user-manual/07-input-and-output.md | 368 +++ docs/user-manual/08-the-screen-and-colour.md | 412 +++ docs/user-manual/09-graphics.md | 388 +++ docs/user-manual/10-sound.md | 196 ++ docs/user-manual/11-data-files-and-devices.md | 347 +++ docs/user-manual/12-errors-and-debugging.md | 292 +++ .../user-manual/13-memory-and-machine-code.md | 298 +++ docs/user-manual/14-system-variables.md | 457 ++++ docs/user-manual/15-expert-techniques.md | 332 +++ docs/user-manual/README.md | 106 + docs/user-manual/appendix-a-keywords-a-l.md | 1322 ++++++++++ docs/user-manual/appendix-a-keywords-m-z.md | 1104 +++++++++ docs/user-manual/appendix-b-error-messages.md | 149 ++ .../user-manual/appendix-c-character-codes.md | 221 ++ .../appendix-d-tokens-and-priorities.md | 237 ++ 70 files changed, 45906 insertions(+), 10 deletions(-) create mode 100644 annotated/README.md create mode 100644 annotated/assign.asm create mode 100644 annotated/check.sh create mode 100644 annotated/do.asm create mode 100644 annotated/editor.asm create mode 100644 annotated/endprint.asm create mode 100644 annotated/equates.asm create mode 100644 annotated/eval.asm create mode 100644 annotated/fn.asm create mode 100644 annotated/fpcmain.asm create mode 100644 annotated/grabput.asm create mode 100644 annotated/graph0.asm create mode 100644 annotated/graph1.asm create mode 100644 annotated/graph2.asm create mode 100644 annotated/list.asm create mode 100644 annotated/lookvar.asm create mode 100644 annotated/main.asm create mode 100644 annotated/mainlp.asm create mode 100644 annotated/misc1.asm create mode 100644 annotated/misc2.asm create mode 100644 annotated/misc31.asm create mode 100644 annotated/misc32.asm create mode 100644 annotated/miscx1.asm create mode 100644 annotated/miscx2.asm create mode 100644 annotated/mult.asm create mode 100644 annotated/nparpro.asm create mode 100644 annotated/printfp.asm create mode 100644 annotated/roll.asm create mode 100644 annotated/rom1fns.asm create mode 100644 annotated/romtest.asm create mode 100644 annotated/samrom.asm create mode 100644 annotated/scrfn.asm create mode 100644 annotated/scrsel1.asm create mode 100644 annotated/scrsel2.asm create mode 100644 annotated/tadjm.asm create mode 100644 annotated/tapemn.asm create mode 100644 annotated/tapex.asm create mode 100644 annotated/text.asm create mode 100644 annotated/tprint.asm create mode 100644 annotated/transend.asm create mode 100644 annotated/using.asm create mode 100644 annotated/vars.asm create mode 100644 annotated/verify_annotated.py create mode 100644 docs/extending-basic.md create mode 100644 docs/font-rendering.md create mode 100644 docs/hudg.md create mode 100644 docs/user-manual/01-first-steps.md create mode 100644 docs/user-manual/02-the-editor.md create mode 100644 docs/user-manual/03-numbers-strings-and-variables.md create mode 100644 docs/user-manual/04-expressions-and-operators.md create mode 100644 docs/user-manual/05-decisions-and-loops.md create mode 100644 docs/user-manual/06-procedures-and-functions.md create mode 100644 docs/user-manual/07-input-and-output.md create mode 100644 docs/user-manual/08-the-screen-and-colour.md create mode 100644 docs/user-manual/09-graphics.md create mode 100644 docs/user-manual/10-sound.md create mode 100644 docs/user-manual/11-data-files-and-devices.md create mode 100644 docs/user-manual/12-errors-and-debugging.md create mode 100644 docs/user-manual/13-memory-and-machine-code.md create mode 100644 docs/user-manual/14-system-variables.md create mode 100644 docs/user-manual/15-expert-techniques.md create mode 100644 docs/user-manual/README.md create mode 100644 docs/user-manual/appendix-a-keywords-a-l.md create mode 100644 docs/user-manual/appendix-a-keywords-m-z.md create mode 100644 docs/user-manual/appendix-b-error-messages.md create mode 100644 docs/user-manual/appendix-c-character-codes.md create mode 100644 docs/user-manual/appendix-d-tokens-and-priorities.md diff --git a/annotated/README.md b/annotated/README.md new file mode 100644 index 0000000..ab8c0ab --- /dev/null +++ b/annotated/README.md @@ -0,0 +1,238 @@ +# Annotated SAM Coupé ROM 3.0 source + +A parallel copy of the ROM source, documented as a modern codebase would be: file-level overviews, per-routine +contracts (entry, exit, registers used, notes), inline explanation of non-obvious code, and named constants in place +of magic numbers. + +**The annotated source is intended to assemble to a byte-identical ROM image.** Nothing here changes behaviour. + +> [!WARNING] +> This annotation was produced with AI assistance and has not been assembled on real hardware (see +> [Verification](#verification) for what *has* been checked). Treat the commentary as a well-evidenced reading of the +> code, not as the author's own documentation. + +## Status + +**Complete.** All 39 source files have been converted, plus the new `equates.asm`. The annotated tree assembles to +an image byte-identical to the one built from the original source. + +| File | Lines (orig → annotated) | Contents | +|---|---|---| +| `samrom.asm` | 49 → 73 | Build glue; include order changed (see below) | +| `equates.asm` | — → 670 | **New file**: named constants | +| `vars.asm` | 527 → 655 | System variable map; adds legacy token aliases | +| `main.asm` | 552 → 872 | Restarts, interrupt entries, public jump table, inter-ROM linkage | +| `lookvar.asm` | 328 → 372 | Variable lookup; documents both variable storage layouts | +| `misc1.asm` | 507 → 623 | Streams and channels, colour state, READ, POKE, the 0-512K address model | +| `editor.asm` | 648 → 786 | The line editor, channel R, DEF KEY expansion | +| `list.asm` | 657 → 800 | AUTOLIST, LIST, CLS, PRINT, listing cursor movement | +| `mainlp.asm` | 775 → 862 | Interpreter main loop, syntax pass, line insertion, error handling | +| `eval.asm` | 894 → 1004 | Expression evaluator, operator priorities, literal conversion | +| `do.asm` | 823 → 967 | DO/LOOP, IF/ELSE, FOR/NEXT, BASIC stack, line finder | +| `grabput.asm` | 472 → 543 | GRAB and PUT block graphics, FARLDIR cross-page block move | +| `tadjm.asm` | 907 → 1114 | Calculator stack, program searcher, MAKEROOM/RECLAIM, page arithmetic, tape edges | +| `assign.asm` | 1004 → 1156 | Assignment, array indexing, string slicing, DIM | +| `fn.asm` | 995 → 1097 | DEF FN/FN, DEF PROC/PROC, LOCAL, the call-buffer compile pass | +| `nparpro.asm` | 772 → 872 | PROC parameter binding by value and by REF, RESTORE, local teardown | +| `misc2.asm` | 997 → 1137 | RST 8 dispatch and DOS hand-off, ROM1-to-RAM stubs, LET, RUN/CLEAR, syntax helpers | +| `endprint.asm` | 988 → 1160 | Per-mode character rendering, screen and pixel addressing, the fixed routines | +| `graph0.asm` | 529 → 642 | CIRCLE and DRAW: midpoint circle, Bresenham line, off-screen checking | +| `graph1.asm` | 313 → 380 | PLOT and the per-mode, per-`OVER` pixel plot routines dispatched through IY | +| `graph2.asm` | 1093 → 1333 | BLITZ, flood FILL and its check screen, coordinate ranges and offsets, the graphics recorder | +| `roll.asm` | 847 → 1140 | ROLL and SCROLL, the editor's window scroller, mode 0 scan stepping | +| `fpcmain.asm` | 766 → 844 | Floating-point calculator: opcode table, fetch-execute loop, stack primitives | +| `mult.asm` | 827 → 987 | Multiply, divide, add, subtract; integer fast paths and the 32-bit mantissa loops | +| `transend.asm` | 407 → 521 | SIN, COS, TAN, EXP, LN, ATN, ASN, ACS and `^`, as calculator bytecode | +| `printfp.asm` | 564 → 682 | Number to decimal string: shift-and-DAA integer conversion, ×10 fraction conversion | +| `miscx2.asm` | 517 → 589 | DEF KEYCODE, DEF FN, **the tokeniser**, MERGE | +| `miscx1.asm` | 782 → 923 | The RAM-executed routines: RENUMBER, GET, DELETE, KEYIN, POP, INPUT | +| `misc31.asm` | 695 → 812 | CALL, cold-start RAM sizing and initialisation, NEW, font unpacking, PALETTE | +| `misc32.asm` | 653 → 811 | BEEP and the sound effects, colour items, BORDER, WINDOW, RANDOMIZE | +| `scrsel2.asm` | 663 → 815 | GOTO/GOSUB, the keyword matcher, MODE and CSIZE, AUTO, SOUND, the disk boot loader | +| `scrfn.asm` | 774 → 918 | COPY, `SCREEN$`, listing output, the pretty-listing indent machinery | +| `tapex.asm` | 567 → 720 | Tape and network block transfer: pulse encoding, leader detection, bit timing | +| `using.asm` | 842 → 997 | Pointer adjustment after memory moves, printer output, heap, INSTR, LENGTH, `STRING$`, curved DRAW | +| `tprint.asm` | 832 → 1000 | The print path: ASCII, tokens, UDGs, control codes, scrolling | +| `rom1fns.asm` | 877 → 1193 | VAL, RND, ATTR, POINT, the string producers, SQR/ABS/SGN/INT/TRUNCATE, UDG | +| `scrsel1.asm` | 878 → 1125 | Screens and streams, page allocation, the interrupt handler, the keyboard scanner | +| `tapemn.asm` | 929 → 1134 | SAVE/LOAD/VERIFY/MERGE: header building, matching and reconciliation | +| `text.asm` | 1406 → 1589 | Messages and their compression, the keyword table, dispatch tables, key map, character set | +| `romtest.asm` | 110 → 170 | Standalone: compare the assembled image against the live ROM | + +`annotated/samrom.asm` includes every file in the directory, so the tree builds on its own; the check script still +overlays it onto the originals so the two images can be compared side by side. + +## Conventions + +### File header + +Each file opens with a banner giving its purpose, the routines it contains grouped by theme, and any structural +constraint a reader needs up front (paging assumptions, why a body is copied to RAM before execution, and so on). + +### Routine header + +```asm +; --------------------------------------------------------------------------------------------------------------------- +; NAME -- one-line summary +; +; Longer explanation of the algorithm and why it is written this way. +; +; Entry: A = ... +; HL = ... +; Exit: BC = ... +; CY set if ... +; Notes: ... +; --------------------------------------------------------------------------------------------------------------------- +``` + +Entry/Exit are stated wherever the original comments or the call sites make the contract determinable. Where a routine +has several entry points, each is documented at its label. + +### Layout + +* Labels in column 0, mnemonics in column 13, comments aligned at column 41 where practical. +* Maximum line length 120 characters. +* Section banners use `=` rules, routine headers `-` rules. + +### Naming + +New constants are prefixed so they can never collide with the original symbols, none of which contain an underscore: + +| Prefix | Meaning | Example | +|---|---|---| +| `ERR_` | Error/report code passed after `RST &08` | `ERR_NONSENSE` (29) | +| `HOOK_` | DOS hook code | `HOOK_AUTOLOAD` (136) | +| `TOK_` | Single-byte BASIC keyword token | `TOK_REM` (&B7) | +| `FN_` | Function code following the `&FF` prefix | `FN_SIN` (&53) | +| `F...` | Bit mask within a named flag byte | `FFLAGRUN`, `FTVAUTOLIST` | +| `LMPR`/`VID` | Paging and video port bit values | `LMPRROM1`, `VIDMODE` | +| `CC_` | Print control code | `CC_AT` (22) | +| `FT_` | Save/load file type | `FT_BASIC` (16) | +| `RDIR`/`RS` | ROLL/SCROLL direction and mode | `RDIRDOWN` (4), `RSROLL` (&FF) | +| `SCLIST` | Field within a screen list entry | `SCLISTPAGE` (%00011111) | +| `KB` | Keyboard queue and matrix geometry | `KBQMASK` (7), `KBSHCTRL` (3) | +| `BLZ` | BLITZ command code | `BLZCIRCLE` (3) | +| `SKIP...` | Opcode literal used as a "jump over the next instruction" | `SKIP1CP` (&FE) | + +The handful of constants the original source already named (`SAVETOK`, `THENTOK`, `LRPORT`, …) keep those names. +Token names are retained in `vars.asm` as aliases of the systematic `TOK_`/`FN_` definitions, so both spellings +resolve to the same value and older listings stay readable. + +### Include order + +`annotated/samrom.asm` pulls `equates.asm` and `vars.asm` in **before** `main.asm`, where the original included +`vars.asm` second. Both files contain nothing but `EQU` definitions and emit no bytes, so the image is unaffected — +and every module can now refer to ports, tokens and system variables by name rather than by literal. + +## Verification + +### Confirmed by assembly + +pyz80 1.3.0 builds the annotated tree to an image **byte-identical** to the original. `check.sh` performs the whole +comparison: + +```sh +python -m pip install pyz80 +annotated/check.sh +``` + +```text +samrom.asm (40 annotated files): BYTE-IDENTICAL +romtest.asm: BYTE-IDENTICAL + +verify_annotated.py: FAILURES: 0 +``` + +It builds `samrom.asm` from the repository root and from `annotated/`, compares the binaries, does the same for +`romtest.asm` — which `samrom.asm` does not include — and then runs the static checker, which reports *which line* +differs when something does. Exit status is 0 only if every comparison matched, so it drops straight into a hook or +a CI step. Run it after every edit. + +Options: `--overlay` copies `annotated/*.asm` over a copy of the originals and builds that instead, which is what +made it possible to verify one file at a time during the conversion and is still the right mode for a partial +tree; `--no-verify` skips the static checker; `--keep` leaves the build directory behind. + +The script finds pyz80 itself, including in the per-user scripts directory Windows installs it into. Note that the +pip package provides a console script called `pyz80`, **not** `pyz80.py`; the repository's `Makefile` and +`make.bat` still invoke the older script name. + +The annotated tree is self-contained, so the default mode is a genuine standalone build. Both the reordered +includes and the forward-referenced constants assemble cleanly, so the two open questions noted below are now +closed. + +Note that the pip package installs a `pyz80` console script, **not** `pyz80.py`; the repository's `Makefile` and +`make.bat` still invoke the older script name. + +`romtest.asm` is not reachable from `samrom.asm`, so it is checked by building it separately in both trees and +comparing the two images the same way. + +### Pre-existing source/image discrepancy + +The assembled original differs from `roms/ROM30` in exactly three bytes, at ROM1 &FAB8-&FABA: the source spells the +copyright banner `MILES GORDON TECHNOLOGY PLC` where the shipped image has `plc`. This predates the annotation and +is unaffected by it — but it does mean the source is not quite a byte-exact reconstruction of the official image, +despite what `ReadMe.txt` implies. + +### Static equivalence check + +`verify_annotated.py` checks equivalence directly from the text, which is useful for spotting mistakes without a +full build and for pinpointing *which line* differs when a build does fail: + +1. Strip comments and blank lines. +2. Build a symbol table from every `EQU` in both trees and resolve each to an integer. +3. Normalise each code line: canonicalise numeric literals (`&FF`, `%11111111`, `255`, `"A"`) to decimal, substitute + resolved symbols for their values, constant-fold arithmetic operands, and standardise whitespace and case. +4. Compare the resulting instruction streams line for line. + +If the streams match, the two files must assemble identically — no annotation or renaming can have altered the +emitted bytes. Memory indirection is distinguished from arithmetic so `LD HL,(CHAD)` is never confused with a folded +constant; folded values are compared modulo 65536 so `-SCANBYTESM23` and `&FF80` are recognised as the same word; +and includes of files that emit nothing are ignored so the reordering above is not flagged. + +```sh +python annotated/verify_annotated.py . annotated +``` + +```text + -- equates.asm: no original (new file, skipped) + OK main.asm (386 code lines) + OK roll.asm (623 code lines) + ... + OK vars.asm (0 code lines) + +FAILURES: 0 +``` + +Two parsing bugs in the checker had to be fixed before it agreed with the assembler: an apostrophe was treated as a +string quote even in `EX AF,AF'`, which swallowed the following comment, and the numeric-literal pattern matched +*inside* identifiers, so `SKIP1LDH` was read as `SKIP1L` plus the suffix-hex literal `DH`. Both produced false +mismatches on files the assembler had already confirmed byte-identical. + +The check has teeth: it was validated by seeding a one-byte change (`LD A,&FE` → `LD A,&FD`), which it located +immediately, and it caught a genuine error during the conversion of `main.asm`, where the `MODE` range check's +`LD DE,&0400+34` had been rewritten with the wrong error constant (34 is `ERR_BADMODE`, not `ERR_IOOR`). + +A separate cross-check confirms all 508 symbols defined in the original tree resolve to identical values in the +annotated tree. + +### What it does not prove + +* That the ROM behaves correctly on hardware. A byte-identical image cannot behave differently, but neither the + original nor the annotated build has been run on a real machine as part of this work. `romtest.asm` is the tool + for that check: it compares the assembled image against the live ROM from within a running machine. +* That the commentary is correct. The build proves the *code* is unchanged, not that the explanations of it are + right — see the warning at the top. + +## Related documentation + +Prose documentation of the same material lives in [`../docs`](../docs): + +* [source-files.md](../docs/source-files.md) — per-file routine reference +* [constants.md](../docs/constants.md) — every `EQU` in the original source +* [memory-map.md](../docs/memory-map.md) — system page and BASIC area layout +* [machine-code-interface.md](../docs/machine-code-interface.md) — jump table, calculator, `CALL`/`USR` +* [extending-basic.md](../docs/extending-basic.md) — the interpreter's extension hooks, with worked examples +* [tokenized-program-format.md](../docs/tokenized-program-format.md) — tokeniser and stored program format +* [file-formats.md](../docs/file-formats.md) — saved file header and layout +* [font-rendering.md](../docs/font-rendering.md) — character cell geometry +* [hudg.md](../docs/hudg.md) — character set pointers diff --git a/annotated/assign.asm b/annotated/assign.asm new file mode 100644 index 0000000..51ea119 --- /dev/null +++ b/annotated/assign.asm @@ -0,0 +1,1172 @@ +; ===================================================================================================================== +; ASSIGN.ASM -- Assignment, array indexing, string slicing and DIM +; ===================================================================================================================== +; +; Everything that writes to a variable, and everything that works out where in a variable to write. +; +; THE DESTINATION DESCRIPTOR +; -------------------------- +; Before any assignment, SYNTAX1 (or SYNTAX4 for a FOR variable) looks the name up and records what it found: +; +; DEST/DESTP the value's address -- or, for a variable that does not exist yet, the place where its record +; should be linked in +; STRLEN the destination length for an existing string, or the type byte otherwise +; FLAGX bit 0 set when the variable is new +; DFTFB zero when an existing numeric holds "minus zero", which DEFAULT treats as not existing +; +; ASSIGN then acts on that description. Numerics either overwrite five bytes or create a new chain entry; strings +; are more involved, because their length can change. +; +; STRINGS OF FIXED AND VARIABLE LENGTH +; ------------------------------------ +; A slice, or an element of a string array, has a fixed size: the value is copied in, truncated or space-padded to +; fit. A simple string can change size, so assignment creates a whole new record at the end of the string area and +; then deletes the old one. Doing it in that order is what makes LET a$ = a$ + "x" work. +; +; ===================================================================================================================== + + +; --------------------------------------------------------------------------------------------------------------------- +; VALFET1 / VALFET2 -- evaluate a value and assign it +; +; Entry: VALFET1 with the destination already assessed; VALFET2 with the expected type in A. +; Exit: The value is stored, or the types did not match and ERR_NONSENSE was raised. +; --------------------------------------------------------------------------------------------------------------------- + +VALFET1: LD A,(FLAGS) + +VALFET2: PUSH AF + CALL SCANNING + LD A,(FLAGS) + LD D,A + POP AF + XOR D + AND FFLAGNUM + JP NZ,NONSENSE ; A string cannot be assigned to a number, or the reverse + + LD A,D + RLA + RET NC ; Syntax check only + + +; --------------------------------------------------------------------------------------------------------------------- +; ASSIGN -- store the calculator stack top into the described destination +; --------------------------------------------------------------------------------------------------------------------- + +ASSIGN: CALL ASSISR + JP SELCHADP ; The store may have changed the page + + +; --------------------------------------------------------------------------------------------------------------------- +; CGXRG -- halve or double the X range pseudo-variable +; +; Called when FATPIX or MODE changes the pixel width, so that graphics coordinates keep referring to the same place. +; +; Entry: HL -> the XRG value, A = 0 to double or non-zero to halve. +; --------------------------------------------------------------------------------------------------------------------- + +CGXRG: PUSH HL + CALL HLTOFPCS + LD B,A + + DB CALC ; XRG + DB ONELIT + DB 2 ; XRG, 2 + DB STKBREG ; XRG, 2, flag + DB JPFALSE ; Zero means double, so jump to the multiply + DB 4 + + DB DIVN ; XRG / 2 + DB JUMP + DB 2 + + DB MULT ; XRG * 2 + DB EXIT + + POP DE + JR ASENV ; Drop the result and copy it back into the variable + + +; --------------------------------------------------------------------------------------------------------------------- +; CRTVAR35 / CRTVAR4 -- create a numeric variable from a name already in the buffer +; +; Entry: CRTVAR35 from PROC parameter processing; CRTVAR4 (also used by MERGE and SETUPVARS) with A = the type byte. +; The value is on the calculator stack and the name is at FIRLET. +; --------------------------------------------------------------------------------------------------------------------- + +CRTVAR35: LD A,(TLBYTE) + +CRTVAR4: LD C,A + LD HL,FLAGS + SET 6,(HL) ; FFLAGNUM + + CALL NUMLOOK + CALL SYN14C ; Build the destination descriptor from the search result + + +; --------------------------------------------------------------------------------------------------------------------- +; ASSISR -- the assignment proper +; +; Entry: DEST, DESTP, STRLEN and FLAGX describe the destination; the value is on the calculator stack. +; --------------------------------------------------------------------------------------------------------------------- + +ASSISR: CALL ADDRDEST ; For a new numeric this is the previous link; for an existing one the + ; value; for a new string the area terminator; for an existing string its + ; first character + LD A,(FLAGS) + ADD A,A + LD A,(FLAGX) + JP P,ASSTR ; A string + + EX DE,HL + RRA + JR C,ASNN ; FFLXNEWVAR: the numeric does not exist yet + + +; --------------------------------------------------------------------------------------------------------------------- +; ASENV -- overwrite an existing numeric +; +; Entry: DE -> the five bytes in the variables area. +; --------------------------------------------------------------------------------------------------------------------- + +ASENV: CALL FDELETE ; Drop the value, leaving HL pointing at it + +LDI5: LD BC,NUMVALSIZE + LDIR + RET + + +; --------------------------------------------------------------------------------------------------------------------- +; ASNN -- create a new numeric variable +; +; Entry: DE -> the low byte of the last link in this letter's chain. +; Notes: The new record goes at NUMEND, so the link is set to the displacement from itself to there. The link +; cannot be written until the space is known to exist, hence the two-stage approach. +; --------------------------------------------------------------------------------------------------------------------- + +ASNN: LD A,(DESTP) + AND LMPRPAGE + LD C,A + LD HL,(NUMEND) + LD A,(NUMENDP) + SUB C + JR Z,ANSP ; The usual case: both are in the same page + + LD BC,&4000 + +ANSPL: ADD HL,BC ; Bring NUMEND into the same frame of reference as the link, so the + DEC A ; subtraction below is meaningful. Wrapping past 64K does not matter, + JR NZ,ANSPL ; since a chain never spans that much. + + AND A + +ANSP: SBC HL,DE ; Displacement from the link's low byte to the first free byte + DEC HL ; ... measured from its high byte, as the link format requires + PUSH HL ; Keep it: the link cannot be written until the space is secured + + LD A,(NUMENDP) + LD C,A + LD HL,(SAVARS) + LD A,(SAVARSP) + CP C ; The string area is always the higher of the two + JR Z,ABSP ; Unusually, both are in the same page + + SET 6,H ; One page apart, so add 16K to compare them + +ABSP: LD BC,(NUMEND) + SBC HL,BC + EX DE,HL ; DE = the free gap, HL = the link address + LD A,D + AND A + JR NZ,ANOK ; At least 256 bytes free + + LD A,E + CP 60 + JR NC,ANOK ; At least 60, which is the largest a numeric record can be + + CALL ADDRSAV ; Not enough: open more space before the string area + CALL DECPTR + LD BC,&0200 + CALL MAKEROOM + CALL ADDRDEST ; The move may have shifted the link, so fetch it again + +; --- ANOK: there is room, so commit --- + +ANOK: POP DE + LD (HL),E ; Point the previous last variable of this letter at the new one + INC HL + LD (HL),D + CALL ADDRNE ; HL -> NUMEND, where the record goes + LD A,(TLBYTE+33) + LD (HL),A ; Type and name length + INC HL + LD B,&FF + LD (HL),B + INC HL + LD (HL),B ; A link of &FFFF: this is now the last of its letter + INC HL + EX DE,HL + LD HL,FIRLET+34 ; The second letter of the name + AND TLNAMELEN + JR Z,ASNCL ; A one-letter name + + LD C,A + INC B ; BC = the rest of the name + LDIR + +ASNCL: CALL ASENV ; Copy the value in + + +; --------------------------------------------------------------------------------------------------------------------- +; NELOAD -- set NUMEND past a newly created variable +; --------------------------------------------------------------------------------------------------------------------- + +NELOAD: LD (NUMEND),DE + BIT 6,D + RET Z ; Still inside the window + + RES 6,D ; Crossed 16K, so advance the page + LD A,(NUMENDP) + INC A + LD (NUMENDP),A + JR NELOAD + + +; ===================================================================================================================== +; ASSTR -- assign to a string +; ===================================================================================================================== + +ASSTR: RRA + JP C,ASNST ; FFLXNEWVAR: create it + +; --- An existing string --- + + LD BC,(STRLEN) ; The destination length + LD A,(DESTP) + RLA + JR C,ASDEL ; Bit 7 set: a simple string, so replace the whole record + +; --- A slice or array element: fixed size, so copy in place --- + + LD A,B + OR C + RET Z ; A zero-length destination, as in LET a$(4 TO 3) = "test" + + PUSH HL ; The destination + PUSH BC ; Its size + CALL STKFETCH ; ADE = the source, BC = its length + POP HL ; The destination size + SBC HL,BC ; NC here + JR NC,AES1 ; The source is no longer than the destination + + ADD HL,BC ; It is longer, so truncate it + LD B,H + LD C,L + LD HL,0 ; ... and no padding is needed + +AES1: EX (SP),HL ; Stack the padding count, recover the destination + EX DE,HL ; DE = destination, HL = source + EX AF,AF' + CALL SPLITBC + IN A,(251) + LD C,A ; CDE = the destination + EX AF,AF' ; AHL = the source + CALL FARLDIR + POP BC ; The padding count + LD A,B + OR C + RET Z + +; --- Space-pad the remainder of the destination --- + + LD A,(TEMPB2) + CALL TSURPG ; The page the copy finished in + LD HL,(TEMPW1) ; ... and the address just past it + XOR A + CP C ; NC only when C is zero + ADC A,B ; A = B, or B+1 when C is non-zero: whole passes of the loop below + LD B,C + LD C,A + LD A," " + +ASPSL: LD (HL),A + INC HL + DJNZ ASPSL + + DEC C + RET Z + + CALL CHKHL ; Keep the pointer inside the window + JR ASPSL + + +; --------------------------------------------------------------------------------------------------------------------- +; RECORD -- RECORD TO a$ | RECORD STOP +; +; Arms stream 16 so that printing to it appends to the named string, and makes graphics commands append BLITZ +; records to it as well. +; --------------------------------------------------------------------------------------------------------------------- + +RECORD: CP TOK_STOP + JR NZ,RECORD2 + + XOR A + LD (GRARF),A ; Graphics recording off + RST &20 + RET + +RECORD2: CP TOTOK + JR NZ,RCNONS + + RST &20 + CALL LVFLAGS + JP M,NONSENSE ; A numeric target makes no sense + + JR C,RECORD3 ; Running + + BIT 6,C + RET Z ; Syntax check: a simple string is acceptable + +RCNONS: RST &08 + DB ERR_NONSENSE + +RECORD3: EX AF,AF' + CALL NZ,ASDEL2 ; Delete any existing variable of that name + + LD DE,STRM16NM + CALL SCOPN1 ; Record the name where the stream 16 driver can find it. + ; This may copy 12 bytes and touch GRARF, which is harmless since GRARF is + ; set immediately afterwards. + LD A,D + LD (GRARF),A ; Graphics recording on + CALL SCOPNM ; Exits with BC = 0 + JR ASNS1 ; Create the variable as a null string + + +; --------------------------------------------------------------------------------------------------------------------- +; ASDEL -- assign a simple string, then delete the previous version +; +; The new value is built first, so that LET a$ = a$ and LET a$ = a$ + "x" both work. +; --------------------------------------------------------------------------------------------------------------------- + +ASDEL: CALL ASNST ; Create the new record at the end of the string area + CALL ADDRDEST ; HL -> the text of the old one + LD DE,-STRHDRSIZE + ADD HL,DE ; -> its type byte + CALL CHKPTR + JR ASDEL3 + + +; --------------------------------------------------------------------------------------------------------------------- +; ASDL1 / ASDEL2 / ASDEL3 -- delete a string or array record +; +; Entry: ASDL1 with A = the page; ASDEL2 with STRLOCN pointing at it and its page mapped; +; ASDEL3 with HL pointing at it. +; --------------------------------------------------------------------------------------------------------------------- + +ASDL1: CALL SELURPG + +ASDEL2: LD HL,(STRLOCN) + +ASDEL3: PUSH HL + LD BC,STRHDRSIZE-3 + ADD HL,BC ; -> the length in pages + CALL ADD14 + LD B,H + LD C,L + POP HL + JP RECL2BIG ; Remove the data and its 14-byte header together + + +; --------------------------------------------------------------------------------------------------------------------- +; ADD14 -- read a record's data length and add the header size +; +; Entry: HL -> the length in pages. +; Exit: AHL = the total record length. +; --------------------------------------------------------------------------------------------------------------------- + +ADD14: LD A,(HL) + INC HL + LD E,(HL) + INC HL + LD D,(HL) + EX DE,HL ; AHL = the data length + LD BC,STRHDRSIZE + ADD HL,BC + BIT 6,H + RET Z + + INC A + RET + + +; --------------------------------------------------------------------------------------------------------------------- +; ASNST -- create a new string record at the end of the string area +; +; Entry: The value is on the calculator stack; the name is at TLBYTE+33. +; Notes: A source that lies above WKEND -- a temporary in workspace -- must not be auto-adjusted when MAKEROOM +; moves memory, because it sits above the change point and would be adjusted wrongly. Such a source is +; parked in FIRST/LAST instead of XPTR. +; --------------------------------------------------------------------------------------------------------------------- + +ASNST: CALL STKFETCH ; A = page, DE = start, BC = length + + AND LMPRPAGE + LD H,A + LD A,(WKENDP) + LD L,A + LD A,H + CP L + JR C,ASNS1 ; The source page is below WKEND's + + JR NZ,ASNS0 ; ... or above it + + LD HL,(WKEND) + SBC HL,DE + JR NC,ASNS1 ; Same page, and the source is at or below WKEND + +ASNS0: LD (FIRST),DE ; Above WKEND: remember it somewhere that is not auto-adjusted + LD (LAST),A + LD A,&FF ; Signal that XPTR is not in use + +; --- ASNS1: also entered by RECORD, to create a null string --- + +ASNS1: PUSH AF + PUSH BC ; The text length + LD (XPTR),DE ; Park the source where MAKEROOM will adjust it if it moves + LD (XPTRP),A + LD A,STRHDRSIZE ; Type byte, 10 name characters and the 3-byte length + ADD A,C + LD C,A + JR NC,ASNS2 + + INC B + JP Z,STLERR ; The whole record must fit in 65535 bytes + +ASNS2: LD A,B + RLCA + RLCA + AND &03 ; A = whole 16K pages, BC = the remainder + CALL SAROOM ; Open the space and write the type byte and name + POP BC ; The text length + CALL MBC ; Write the length into the record + IN A,(251) + LD C,A ; CDE = the destination + POP AF + INC A + LD A,(XPTRP) + LD HL,(XPTR) + JR NZ,ASNS3 ; XPTR was in use, so it holds the adjusted source + + LD A,(LAST) ; Otherwise the source was parked in FIRST/LAST + LD HL,(FIRST) + +ASNS3: LD (XPTR+1),A ; The page has bit 7 clear, which cancels the '?' marker + JP FARLDIR + + +; ===================================================================================================================== +; Assessing a destination +; ===================================================================================================================== + +; --------------------------------------------------------------------------------------------------------------------- +; SYNTAX4 -- assess a FOR control variable +; +; A FOR variable must be a simple numeric. Any ordinary variables of the same name are marked unused, so that the +; FOR record replaces them rather than shadowing them. +; --------------------------------------------------------------------------------------------------------------------- + +SYNTAX4: CALL LVFLAGS + JP P,NONSENSE ; A string + + BIT 5,C + JP NZ,NONSENSE ; A numeric array + + JR NC,SYNT41 ; Syntax check + + EX AF,AF' + +SYN42: JR Z,SYN14C ; It does not exist yet + + BIT 6,C ; TLFORVAR: it is already a FOR variable, so reuse it + JR NZ,SYN14C + + PUSH IX + POP HL ; -> the link + SET 5,(IX-1) ; Mark the ordinary variable unused + CALL NVMLP ; Keep searching the chain + JR SYN42 ; ... and mark every copy + +SYNT41: EX AF,AF' + + JR SYN14C + + +; --------------------------------------------------------------------------------------------------------------------- +; SSYNTAX1 / SYNTAX1 -- assess a destination for LET, READ or INPUT +; --------------------------------------------------------------------------------------------------------------------- + +SSYNTAX1: RST &20 + +SYNTAX1: CALL LOOKVARS + + +; --------------------------------------------------------------------------------------------------------------------- +; SYN14C / SYN1PP -- build the destination descriptor from a search result +; +; Entry: DE -> the value if found; C = the type byte, from the variables area if found or the wanted one if not. +; Exit: DEST, DESTP, STRLEN, FLAGX and DFTFB all set. +; --------------------------------------------------------------------------------------------------------------------- + +SYN14C: EX DE,HL + +; --- SYN1PP: entered from PROC parameter processing --- + +SYN1PP: LD HL,FLAGX + LD (HL),0 ; Assume the variable exists + JR NZ,TSYNT12 ; It does, or this is a syntax check + + INC (HL) ; FFLXNEWVAR + + LD A,C + AND TLARRAY + JR Z,TSYNT14 ; A simple string or number: DESTP bit 7 stays clear + +VNFERR: RST &08 ; An undimensioned array, or a slice of a string that does not exist + DB ERR_NOTFOUND + +; --- The variable exists, or this is a syntax check --- + +TSYNT12: LD A,(FLAGS) + ADD A,A + JP P,TSYNT13 ; A string + + BIT 5,C + JR Z,TSYNT14 ; A simple number + +TSYNT13: CALL STKVAR ; Index the array or stack the string details + LD A,(FLAGS) + ADD A,A + JP M,TSYNT15 ; A numeric array element: HL -> it, its page mapped + + CALL C,STKFETCH ; A string: DE = start, BC = length, A = page with bit 7 set when the old + EX DE,HL ; copy must be deleted, as in LET a$ = "ss" + JR TSYN16 + +TSYNT14: EX DE,HL + +; --- Record the result. For numerics and new variables STRLEN holds the type byte and junk; for existing strings +; --- and arrays it holds the length. + +TSYNT15: IN A,(251) + AND LMPRPAGE + +TSYN16: LD (STRLEN),BC + LD (DEST),HL + LD (DESTP),A + LD B,(HL) + INC HL + LD A,(HL) + INC A + OR B + INC HL + OR (HL) + INC HL + OR (HL) ; Zero only when the value is 00 FF 00 00, i.e. minus zero + LD (DFTFB),A ; Meaningless for a variable that does not exist + + +; --------------------------------------------------------------------------------------------------------------------- +; SCOPNM / SCOPN1 / SCOPN2 -- copy the name to a second buffer +; +; The evaluator will reuse TLBYTE and FIRLET, so the name of the destination is kept at TLBYTE+33. +; --------------------------------------------------------------------------------------------------------------------- + +SCOPNM: LD DE,TLBYTE+33 + +; --- SCOPN1: entered by RECORD TO, which copies to STRM16NM instead --- + +SCOPN1: LD HL,TLBYTE + LD A,(HL) + AND TLNAMELEN ; Name length minus one for a numeric, the true length otherwise + ADD A,2 ; Allow for the type byte and, for a numeric, the first letter + LD C,A + +; --- SCOPN2: entered by the LENGTH function --- + +SCOPN2: LD B,0 + LDIR + JP SELCHADP + + +; ===================================================================================================================== +; STKVAR -- index an array, or stack a string's parameters +; ===================================================================================================================== +; +; Entry: DE -> the length in pages if running, C = the type byte, CY if running. +; CHAD points past the '$' or '(' unless an error follows. +; Exit: For a numeric array, HL -> the element. For a string, its parameters are on the calculator stack. +; --------------------------------------------------------------------------------------------------------------------- + +STKVAR: EX DE,HL + +STKVAR2: JR C,SVRUNT ; Running + +; --- Syntax check: verify the subscript or slicer syntax only --- + + BIT 6,C + JR NZ,SVSSL ; A string array or a sliced string + + BIT 5,C + RET Z ; A simple unsliced string: nothing to check + + DB SKIP1CP ; A numeric array: fall into the subscript check + +SVDSL: RST &20 + +; --- SVDSK: check "n,n,...,n)". Also called by DIM. --- + +SVDSK: CALL EXPT1NUM + CP "," + JR Z,SVDSL + +SVIBH: JP INSISCBRK + + +; --------------------------------------------------------------------------------------------------------------------- +; SVSSL -- check string array or slicer syntax +; +; Accepts "()", "(n,x TO y)", "(n,n, TO y)", "(n,n,y TO)" and the like. +; --------------------------------------------------------------------------------------------------------------------- + +SVSSL: RST &18 + CP ")" + JR Z,SVSL3 ; "()" is allowed + + DB SKIP1CP + +SVSSLP: RST &20 + + CP TOTOK + JR Z,SVSL2 ; A slicer with no first number + + CALL EXPT1NUM + CP "," + JR Z,SVSSLP + + CP TOTOK + JR Z,SVSL2 + + CALL INSISCBRK + JR SLPXHP + +SVSL2: RST &20 ; Skip TO + CP ")" + JR Z,SVSL3 ; "x TO )" + + CALL EX1NUMCB ; "x TO n)" + DB SKIP1CP + +SVSL3: RST &20 + +SLPXHP: JP SLLPEX ; EXPT1NUM left the type as numeric; restore "string" + + +; ===================================================================================================================== +; SVRUNT -- run-time array indexing and string parameter stacking +; ===================================================================================================================== + +SVRUNT: LD A,C + AND TLARRAY + JR NZ,SVARRAYS + +; --- A simple string: convert the stored pages-plus-remainder length to a plain 16-bit one --- + + LD A,(HL) ; Pages, 0-3 + INC HL + LD C,(HL) + INC HL + RRCA + RRCA ; The page count becomes the top two bits + OR (HL) + LD B,A ; BC = the length + LD D,&80 ; Bit 7: delete the old copy after assignment + +SVSIMPLE: EX DE,HL + INC DE ; -> the text + IN A,(251) + BIT 6,D + JR Z,SVSS2 ; The text is in the window + + RES 6,D ; A string starting near &BFFF leaves the pointer in section D + INC A + +SVSS2: AND LMPRPAGE + OR H ; Bit 7 set for a simple string (delete the old copy), clear for a + ; one-dimensional string array element (overwrite in place) + CALL STKSTORE ; FFLAGNUM is already correct + CALL SELCHADP + LD A,(TLBYTE) + BIT 6,A + RET Z ; No bracket followed the name, so no slicing + + JP SLCL2 + + +; --------------------------------------------------------------------------------------------------------------------- +; SVARRAYS -- index an array +; +; The element offset is built up as total = (...(s1 * d2 + s2) * d3 + ...) + sn, computed on the calculator stack, +; then multiplied by the element size and added to the data start. +; --------------------------------------------------------------------------------------------------------------------- + +SVARRAYS: INC HL + INC HL + INC HL + LD B,(HL) ; The number of dimensions + BIT 5,C + JR NZ,SVCDIS ; A numeric array + + DJNZ SVCKS ; A multi-dimensional string array + + LD D,B ; One dimension: D = 0, so bit 7 stays clear meaning "overwrite" + INC HL + LD C,(HL) + INC HL + LD B,(HL) ; BC = the single dimension's length + JR SVSIMPLE ; Treat it exactly like a simple string + +SVCKS: LD A,(TLBYTE) ; A multi-dimensional string array must be referred to with a slicer + AND TLSTRARRAY + JR Z,SWERHP + +SVCDIS: IN A,(URPORT) + PUSH AF ; The page holding the dimension list + PUSH BC ; B = the dimension count, excluding the last for a string array + INC HL + PUSH HL + XOR A + CALL STACKA ; The running total starts at zero + POP HL + POP BC + +SVLOOP: POP AF + PUSH AF + OUT (URPORT),A ; Map the dimension list + PUSH BC + LD C,(HL) + INC HL + LD B,(HL) ; BC = this dimension's size, the subscript limit + INC HL + PUSH HL + PUSH BC + CALL SELCHADP ; Map the program, to evaluate the subscript + CALL STACKBC ; Stack the dimension size + POP BC + CALL GETSUBS ; The subscript, checked against the limit and decremented + +SWERHP: JP NC,SWER2 ; Out of range + + CALL STACKHL + + DB CALC ; total, size, subscript + DB SWOP13 ; subscript, size, total + DB MULT + DB ADDN ; total * size + subscript + DB EXIT + + POP DE ; The dimension list pointer + POP BC ; The dimension counter + RST &18 + DEC B + JR Z,SVEXLP ; All dimensions done + + CP "," + JR NZ,SWER2 ; More dimensions require more subscripts + + RST &20 + EX DE,HL + JR SVLOOP + + +; --------------------------------------------------------------------------------------------------------------------- +; SVEXLP -- all subscripts consumed; compute the element address +; +; Entry: The array's page is on the stack, DE -> the data start. +; --------------------------------------------------------------------------------------------------------------------- + +SVEXLP: BIT 5,C + JR NZ,SVNUMER + +; --- A string array: the last dimension is the element length, and a slicer may follow --- + + POP AF + OUT (URPORT),A + EX DE,HL + LD C,(HL) + INC HL + LD B,(HL) + INC HL + EX DE,HL ; DE -> the data start + PUSH BC ; The element length + CALL SVSR ; AHL = the element's address + POP BC + EX DE,HL + CALL STKST0 ; Stack it; bit 7 clear means "do not erase the original" + CALL SELCHADP + RST &18 + CP ")" + JR Z,SVDIM ; No slicer, as in a$(3) + + CP "," + JR Z,SLCL ; A slicer follows, as in a$(3,2 TO 5) + +SWER2: RST &08 + DB ERR_SUBSCRIPT + +SVDIM: RST &20 + CP "(" + JR NZ,SLLPEX ; Nothing more + +SLCL: RST &20 ; Skip the '(' or ',' + +SLCL2: CALL SLICING + JR SVDIM + +SLLPEX: LD HL,FLAGS + RES 6,(HL) ; FFLAGNUM clear: the result is a string + RET + + +; --- A numeric array: five bytes per element --- + +SVNUMER: CP ")" + JR NZ,SWER2 + + RST &20 + LD BC,NUMVALSIZE + POP AF ; The page + CALL SVSR + JP TSURPG + + +; --------------------------------------------------------------------------------------------------------------------- +; SVSR -- turn an element index into an address +; +; Entry: BC = the element size, DE -> the data start, A = its page, the index on the calculator stack. +; Exit: AHL = the element's address. +; --------------------------------------------------------------------------------------------------------------------- + +SVSR: PUSH AF + PUSH DE + CALL STACKBC + + DB CALC ; total, element size + DB MULT ; the byte offset + DB EXIT + + CALL UNSTLEN ; AHL = it, in page form + POP DE + POP BC + LD C,B ; CDE = the data start + BIT 6,D + JR Z,SVSR2 + + INC C ; ADDAHLCDE ignores bit 6, so account for it here + +SVSR2: JP ADDAHLCDE + + +; ===================================================================================================================== +; SLICING -- evaluate a string slicer +; ===================================================================================================================== +; +; Forms: (), (n), (a TO b), (a TO), ( TO b). An empty result is produced for a reversed range such as (5 TO 2), +; but a genuinely out-of-range value is an error. +; --------------------------------------------------------------------------------------------------------------------- + +SLICING: CALL RUNFLG + CALL C,STKFETCH ; ADE = start, BC = length + + PUSH AF ; The page + RST &18 + POP HL ; H = the page + CP ")" + JR Z,SLSTORE ; "()" means the whole string + + LD (TEMPB2),A ; Non-zero: no subscript error yet + PUSH DE ; The string start + PUSH HL ; H = its page + LD DE,0 ; The default first position + CP TOTOK + JR Z,SLSEC ; "( TO x)" + + CALL GETSUBS ; The first number, checked against the length and decremented + EX DE,HL + RST &18 + CP TOTOK + JR Z,SLSEC + + CP ")" + +NONSH: JR NZ,SWER2 + + LD H,D ; "(n)" means a single character + LD L,E + JR SLDEF + +SLSEC: RST &20 + CP ")" + LD H,B + LD L,C + DEC HL ; Default the second number to the last position + JR Z,SLDEF ; "(x TO )" or "( TO )" + + PUSH DE + CALL GETSUBS ; The second number + POP DE + JR C,SLSE2 ; In range, or a syntax check + + LD A,H + OR L + JR Z,SLND ; "(2 TO 0)" is an empty string, not an error + +SLSE2: PUSH HL + RST &18 + POP HL + CP ")" + JR NZ,NONSH + +SLDEF: SBC HL,DE ; Second minus first; NC here + LD BC,0 ; A reversed range gives an empty string + JR C,SLNUL + + LD A,(TEMPB2) + AND A + JP Z,SWER2 ; A subscript really was out of range + + INC HL + +SLND: LD B,H + LD C,L ; BC = the slice length + +SLNUL: POP AF + POP HL ; AHL = the string start + ADD HL,DE ; ... plus the first position + CALL C,PGOA + BIT 6,H + JR Z,SLDF2 + + RES 6,H + INC A + +SLDF2: EX DE,HL ; ADE = the slice start, BC = its length + LD H,A + +SLSTORE: LD A,(FLAGS) + AND &BF + LD (FLAGS),A ; FFLAGNUM clear: a string + RLA + RET NC ; Syntax check + + LD A,H + JP STKST0 ; Stack it, with bit 7 clear: do not delete the original + + +; ===================================================================================================================== +; DIM -- DIM name(d1,...,dn) +; ===================================================================================================================== +; +; Deletes any existing variable of the same name, then builds a record whose data area holds the dimension count, +; the dimension sizes, and the elements, all cleared. +; --------------------------------------------------------------------------------------------------------------------- + +DIM: CALL LOOKVARS + IN A,(URPORT) ; The page of the existing variable, if there is one + EX AF,AF' ; NZ if it was found + PUSH BC + CALL SCOPNM ; Copy the name where the evaluator cannot disturb it + POP BC + LD A,(TLBYTE) ; The requested type, which may differ from what was found + AND TLARRAY + JP Z,NONSENSE ; DIM needs a bracket + + CALL RUNFLG + JR C,DIMRUN + + CALL SVDSK ; Syntax check: "n,n,...,n)" + +DIM2: CP "," + RET NZ + + RST &20 + JR DIM ; DIM a(8),b(6,5),a$(2) and so on + +DIMRUN: EX AF,AF' + PUSH BC + CALL NZ,ASDL1 ; Delete the existing array + + CALL SELCHADP + POP BC + BIT 5,C + LD BC,1 ; String elements are one byte each + JR Z,DIM4 + + LD C,NUMVALSIZE ; Numeric elements are five + +DIM4: CALL STACKBC ; The running product; B is still zero, so it doubles as the dimension count + DB SKIP1CP + +DIMSZLP: RST &20 ; Skip the comma + + CALL GETSUBS ; The dimension size, returned decremented + INC HL + PUSH HL ; Keep it on the machine stack, behind the counter + PUSH BC + CALL STACKHL + + DB CALC + DB MULT ; 5 * d1 * d2 ..., or 1 * d1 ... + DB EXIT + + POP BC + INC B ; One more dimension + RST &18 + CP "," + JR Z,DIMSZLP + + CALL INSISCBRK + + PUSH BC ; B = the dimension count + LD L,B + LD H,0 + ADD HL,HL ; Two bytes per dimension size ... + INC HL ; ... plus one for the count itself + CALL STACKHL + + DB CALC ; element bytes, dimension info bytes + DB SWOP ; info, elements + DB DUP ; info, elements, elements + DB SWOP13 ; elements, elements, info + DB ADDN ; elements, elements + info + DB DUP + DB ONELIT + DB STRHDRSIZE + DB ADDN ; elements, elements + info, elements + info + 14 + DB EXIT + + CALL UNSTLEN ; ABC = the total record size + CALL SAROOM ; Open it and write the type byte and name; DE -> just past the name + PUSH DE + CALL UNSTLEN ; The size excluding the 14-byte header + EX DE,HL + POP HL + LD (HL),A ; Pages + INC HL + LD (HL),E + INC HL + LD (HL),D ; ... and the remainder + INC HL + POP AF + LD (HL),A ; The dimension count + LD E,A + LD D,0 + ADD HL,DE + ADD HL,DE ; -> the high byte of the last dimension size + LD D,H + LD E,L + +DIMENTLP: POP BC ; The sizes come off the stack in reverse order + LD (HL),B + DEC HL + LD (HL),C + DEC HL + DEC A + JR NZ,DIMENTLP + + PUSH DE + CALL UNSTLEN ; The element area size + CALL AHLNORM ; ... as a 19-bit value + POP DE + EX DE,HL + INC HL ; -> the first byte to clear + LD B,E ; B counts bytes within a 256-byte block + LD E,D + LD D,A ; DE counts the blocks + LD A,B + AND A + JR Z,DIMNAC + + INC DE ; A partial final block, so one more pass. DE is never zero. + +DIMNAC: LD A,(TLBYTE+33) + AND TLSTRARRAY + LD C," " ; String arrays are cleared to spaces + JR NZ,GARC + + LD C,A ; Numeric arrays to zero + +GARC: CALL CHKHL + +DIMCLP: LD (HL),C + INC HL + DJNZ DIMCLP + + DEC DE + LD A,D + OR E + JR NZ,GARC + + CALL SELCHADP + RST &18 + JP DIM2 + + +; --------------------------------------------------------------------------------------------------------------------- +; SAROOM -- open a record at the end of the string area and write its name +; +; Entry: A = pages, BC = the remainder of the total size. +; Exit: DE -> just past the 11-byte type and name field. +; --------------------------------------------------------------------------------------------------------------------- + +SAROOM: PUSH AF + CALL ADDRELND ; The end of the string area is one below the edit line + POP AF + CALL MKRBIG + EX DE,HL + LD HL,TLBYTE+33 + LD BC,11 ; The type byte and ten name characters + LDIR + RET + + +; --------------------------------------------------------------------------------------------------------------------- +; GETSUBS -- evaluate a subscript +; +; Entry: CHAD at the expression, BC = the limit. +; Exit: HL = the value minus one, so the range becomes 0 to limit-1. BC and DE unchanged. +; CY if it was in range. A subscript of zero is always an error. +; --------------------------------------------------------------------------------------------------------------------- + +GETSUBS: PUSH BC + CALL EXPT1NUM + JR C,GTSBC + + POP BC + RET ; Syntax check: a number is all that is required + +; --- GTSBC: run time --- + +GTSBC: CALL GETINT + POP BC ; The limit + OR H + JR Z,SWSIG ; Subscript zero + + DEC HL + SBC HL,BC ; A limit of &FFFF permits 1 to &FFFF + ADD HL,BC + RET C + +SWSIG: XOR A + LD (TEMPB2),A ; Record that a subscript was out of range + RET diff --git a/annotated/check.sh b/annotated/check.sh new file mode 100644 index 0000000..6cbeba2 --- /dev/null +++ b/annotated/check.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# +# check.sh -- prove that the annotated source assembles to the same ROM image as the original. +# +# Builds samrom.asm from the repository root and from annotated/, and compares the two binaries byte for byte. +# romtest.asm is not reachable from samrom.asm, so it is built and compared separately. Finally the static +# equivalence checker is run, which reports *which line* differs when something does. +# +# Usage: annotated/check.sh [options] +# +# --overlay copy annotated/*.asm over a copy of the originals and build that, instead of building +# annotated/ directly. Equivalent while every file is converted; useful if you are working on a +# partial conversion, where the annotated directory does not stand alone. +# --no-verify skip verify_annotated.py (the binary comparison alone is the authoritative check) +# --keep leave the build directory in place and print its path +# +# Exit status is 0 only if every comparison matched. + +set -u + +overlay=0 +verify=1 +keep=0 + +for arg in "$@"; do + case "$arg" in + --overlay) overlay=1 ;; + --no-verify) verify=0 ;; + --keep) keep=1 ;; + -h|--help) sed -n '3,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "check.sh: unknown option '$arg'" >&2; exit 2 ;; + esac +done + +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/.." && pwd) + +# --------------------------------------------------------------------------------------------------------------------- +# Locate pyz80. The pip package installs a console script called "pyz80" (not "pyz80.py", which is what the +# repository's Makefile and make.bat still invoke). On Windows it lands in the per-user scripts directory, which is +# often not on PATH. +# --------------------------------------------------------------------------------------------------------------------- + +if ! command -v pyz80 >/dev/null 2>&1; then + for finder in "sysconfig.get_path('scripts','nt_user')" "sysconfig.get_path('scripts')"; do + dir=$(python -c "import sysconfig;print($finder)" 2>/dev/null) || continue + # python may report a Windows path; make it usable from this shell. + case "$dir" in + [A-Za-z]:\\*) dir="/$(echo "${dir:0:1}" | tr 'A-Z' 'a-z')/$(echo "${dir:3}" | tr '\\' '/')" ;; + esac + if [ -x "$dir/pyz80" ] || [ -f "$dir/pyz80" ]; then + PATH="$PATH:$dir" + export PATH + break + fi + done +fi + +if ! command -v pyz80 >/dev/null 2>&1; then + echo "check.sh: pyz80 not found. Install it with: python -m pip install pyz80" >&2 + exit 2 +fi + +# --------------------------------------------------------------------------------------------------------------------- + +work=$(mktemp -d) || exit 2 +if [ "$keep" -eq 0 ]; then + trap 'rm -rf "$work"' EXIT +fi + +status=0 + +# build