This program renumbers a BASIC program stored in memory, reassigning line numbers starting from 2000 in steps of 10. It works by scanning the program’s tokenized bytecode directly via PEEK, using two DEF FN helpers to read little-endian and big-endian 16-bit words from memory. The routine first performs a pass to collect all GO TO and GO SUB targets (token codes 236 and 237) along with their in-memory addresses into a string T$, encoding each reference as a 9-character record. A second pass then rewrites the line-number header bytes of each program line and patches the numeric literals embedded after GO TO/GO SUB keywords — including the five-byte floating-point representation — using a manual logarithm-based binary exponent calculation to reconstruct the Spectrum’s internal float format.
Program Analysis
Program Structure
The program is divided into two major phases, both beginning at the PROGRAM_START pointer read from system variable address 23635 (PROG):
- Pass 1 (lines 9959–9975): Walks every line of the BASIC program looking for GO TO (
236) and GO SUB (237) tokens. For each one found, it records the in-memory address of the token and the target line number into the accumulator stringT$, using 9-character fixed-width records. - Pass 2 (lines 9976–9999): Walks the program again. For each line below 9958, it rewrites the two-byte line-number header to the new sequential number starting at
BASE=2000with step 10, and calls subroutine 9990 to patch any GO TO/GO SUB references that targeted the old line number.
Line 9958 is a sentinel STOP that prevents the renumberer from processing itself; both passes terminate when they encounter a line number ≥ 9958.
Memory Layout and DEF FN Helpers
Two DEF FN functions abstract 16-bit memory reads:
| Function | Definition | Purpose |
|---|---|---|
FN A(X) | PEEK X + 256*PEEK(X+1) | Little-endian 16-bit read (used for line length and PROG pointer) |
FN B(S) | 256*PEEK S + PEEK(S+1) | Big-endian 16-bit read (used for line numbers in the program area) |
BASIC line headers on the Spectrum are stored as: 2 bytes big-endian line number, 2 bytes little-endian line length, then the tokenized statement bytes. FN B reads the line number and FN A reads the length, allowing the walker to advance by LENGTH+4 bytes per line.
Pass 1: Collecting GO TO/GO SUB References
Lines 9965–9968 scan each line’s token bytes. A line whose first token byte is 234 (the REM token) is skipped entirely, since a REM line’s content is not executable. For every other token equal to 236 (GO TO) or 237 (GO SUB), subroutine 9971 is called.
Subroutine 9971–9975 checks that the number following the keyword has its five-byte float embedded inline (indicated by the number-marker byte 14 at offset I+5). If not, it prints a “NON-STANDARD COMMAND” error and halts. Otherwise it appends a 9-character record to T$:
- Characters 1–5:
STR$ I— the memory address of the token, left as a decimal string padded to 5 characters viaSTR$. - Characters 6–9: bytes at
I+1throughI+4encoded asCHR$— the four ASCII digit characters of the target line number as it appears in the source text.
This means the “line number” stored in the record is the printed decimal digits of the target, not a binary integer — retrieved later with VAL T$(I+5 TO I+8).
Pass 2: Rewriting Line Headers and Patching References
Pass 2 reuses the PROG pointer walk. For each line, the loop at 9980–9982 scans T$ in steps of 9 to find any reference records whose target line number matches the current LINE. If found, subroutine 9990 patches the reference.
The line-header rewrite at 9983–9984 stores the new BASE value as a big-endian 16-bit pair directly into the two header bytes at address S.
Floating-Point Patching Subroutine (9990–9999)
After a GO TO or GO SUB token, the Spectrum stores the target line number twice: once as ASCII digit characters and once as a five-byte internal float (preceded by the marker byte 14). The subroutine must update both representations.
Lines 9991 patches the four ASCII digit bytes using CODE(STR$ BASE)(J) — extracting the J-th character code of the decimal string of BASE. This works correctly only when BASE is exactly a 4-digit number; it would malfunction for 1-, 2-, 3-, or 5-digit targets, which is a known limitation given the fixed step-10 scheme starting at 2000.
Lines 9993–9998 reconstruct the Spectrum’s five-byte float manually:
BYTE1= exponent byte:128 + floor(log₂(BASE) + 1)BYTE2= the mantissa scaled to a 16-bit value- The high byte of the mantissa has its sign bit cleared by subtracting 128 (
INT(BYTE2/256)-128), relying on the fact that the number is positive. - Bytes 4 and 5 of the float (the low mantissa bytes) are left at zero, which is valid for integers up to 16 bits since their contribution is negligible for line-number values.
Notable Techniques and Idioms
- Fixed-width string packing in
T$(9 bytes per record) allows O(n) scanning withSTEP 9and direct field extraction by substring offset. - The use of
VAL T$(I TO I+4)to recover the stored memory address from its decimal string representation is a common Spectrum BASIC idiom for storing integers in strings without binary encoding. - All program lines from 9958 onward are excluded from renumbering, protecting the utility itself.
- The manual float reconstruction avoids any ROM calls, operating purely in BASIC arithmetic.
Bugs and Limitations
- The ASCII digit patching at line
9991assumes the new line number is always exactly 4 digits. Line numbers below 1000 or above 9999 would write incorrect bytes or cause an error. - The float reconstruction only fills 3 of the 5 float bytes; bytes at offsets
MEMORY+9andMEMORY+10(the two lowest mantissa bytes) are never updated. For the range 2000–9950 this is harmless since those bits are zero, but it is architecturally incomplete. - Multi-statement lines containing more than one GO TO or GO SUB are handled correctly by the
FOR Iloop, but a line beginning withREMis skipped in its entirety, meaning a GO TO inside a multi-statement line that starts with REM would be missed. - The renumber step is hardcoded to 10 and the base to 2000; there are no input prompts to customize these values.
Content
Image Gallery
Source Code
9958 STOP
9959 LET T$="": LET X=23635
9960 DEF FN A(X)=PEEK X+256*PEEK (X+1)
9961 DEF FN B(S)=256*PEEK S+PEEK (S+1)
9962 LET S=FN A(X)
9963 LET LINE=FN B(S): IF LINE>=9958 THEN GO TO 9976
9964 LET LENGTH=FN A(S+2)
9965 IF PEEK (S+4)=234 THEN GO TO 9969
9966 FOR I=S+4 TO S+LENGTH+2
9967 IF PEEK I=236 OR PEEK I=237 THEN GO SUB 9971
9968 NEXT I
9969 LET S=S+LENGTH+4
9970 GO TO 9963
9971 IF PEEK (I+5)=14 THEN GO TO 9974
9972 PRINT "NON-STANDARD COMMAND, LINE ";LINE
9973 STOP
9974 LET T$=T$+STR$ I+CHR$ PEEK (I+1)+CHR$ PEEK (I+2)+CHR$ PEEK (I+3)+CHR$ PEEK (I+4)
9975 RETURN
9976 LET BASE=2000: LET X=23635
9977 LET S=FN A(X)
9978 LET LINE=FN B(S): IF LINE>=9958 THEN STOP
9979 LET LENGTH=FN A(S+2)
9980 FOR I=1 TO LEN T$ STEP 9
9981 IF VAL T$(I+5 TO I+8)=LINE THEN GO SUB 9990
9982 NEXT I
9983 POKE S,INT (BASE/256)
9984 POKE S+1,BASE-256*INT (BASE/256)
9985 LET BASE=BASE+10
9986 LET S=S+LENGTH+4
9987 GO TO 9978
9990 FOR J=1 TO 4
9991 POKE (VAL T$(I TO I+4)+J),CODE (STR$ BASE)(J)
9992 NEXT J
9993 LET BYTE1=128+INT (LN BASE/LN 2+1)
9994 LET BYTE2=BASE*65536/(2^(BYTE1-128))
9995 LET MEMORY=VAL T$(I TO I+4)
9996 POKE MEMORY+6,BYTE1
9997 POKE MEMORY+7,INT (BYTE2/256)-128
9998 POKE MEMORY+8,BYTE2-256*INT (BYTE2/256)
9999 RETURN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.