Renum

Date: 198x
Type: Program
Platform(s): TS 2068

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):

  1. 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 string T$, using 9-character fixed-width records.
  2. 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=2000 with 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:

FunctionDefinitionPurpose
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 99659968 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 99719975 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 via STR$.
  • Characters 6–9: bytes at I+1 through I+4 encoded as CHR$ — 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 99809982 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 99839984 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 99939998 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 with STEP 9 and 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 9991 assumes 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+9 and MEMORY+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 I loop, but a line beginning with REM is 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

Appears On

Library tape of the Indiana Sinclair Timex User’s Group.

Related Products

Related Articles

Related 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.

People

No people associated with this content.

Scroll to Top