With File 1, support for mail merging letters.
This program implements a mail-merge system that loads a letter template and a data file from tape, then merges field values from the data file into the letter before printing or displaying the result. The letter template uses special byte markers (131 for field delimiters, 143 for end-of-letter, 144 for a special character, 94 for field references) stored in memory starting at address 38000, while the merged file is assembled at address 32500. A 14-byte machine code routine is POKEd into address 60000 and executed via RANDOMIZE USR to perform fast block memory copies using the Z80’s LDIR instruction (opcode 237, 176), with source and destination addresses patched at runtime by reading the system variables for the RANDOMIZE seed. The program detects whether a printer is attached by reading port 251 (the keyboard/printer port) and supports paginated output with configurable lines-per-page.
Program Analysis
Program Structure
The program is organized into several functional blocks:
- Lines 10–11: Initialization — POKEs a 14-byte machine code routine into address 60000 and jumps to the main menu at line 1000.
- Lines 20–23: Subroutine for printing a slice of
b$and tracking line/page counters (x1,x4). - Line 30: Machine code launcher — patches source and destination addresses into the routine at 60000 and executes it via
RANDOMIZE USR 60000. - Lines 70–72: Input validation subroutine — pads
z$to exactly 10 characters and prompts for confirmation. - Lines 1000–1130: Setup phase — loads the letter template and the data file from tape, measures their lengths, and detects printer presence.
- Lines 1140–1240: Field-scanning loop — scans the letter at 38000 for field delimiter bytes (131), builds an array
b()of field start addresses, then walks the template at 61000 to assemble the merged output at 32500. - Lines 2000–2050: Field substitution — when a caret (ASCII 94) is encountered in the template, reads a numeric field index, looks up the corresponding data in
b(), and copies it into the output buffer. - Lines 3000–3450: Output/rendering engine — reads the assembled output from 32500 and prints it word-wrapped, handling special control bytes for page breaks (139) and form feeds (134), with optional printer copy via
COPY. - Line 5000: End-of-list prompt — offers another copy pass.
- Line 6000:
SAVEline to preserve the program.
Machine Code Routine
Line 10 POKEs 14 bytes into address 60000. The DATA at line 11 decodes as follows:
| Offset | Byte(s) | Z80 Mnemonic | Notes |
|---|---|---|---|
| 0 | 01 00 00 | LD BC, 0 | BC = byte count (patched at runtime, offsets 1–2) |
| 3 | 21 00 00 | LD HL, 0 | HL = source address (patched at offsets 4–5) |
| 6 | 54 | LD D, H | |
| 7 | 5D | LD E, L | DE = HL (destination = source initially, then patched) |
| 8 | 21 00 00 | LD HL, 0 | HL = destination address (patched at offsets 9–10) |
| 11 | ED B0 | LDIR | Block copy BC bytes from HL to DE |
| 13 | C9 | RET |
The routine is a standard Z80 LDIR block copy. The patching mechanism in line 30 is indirect: RANDOMIZE v stores v in the system variable at addresses 23670–23671 (the last RANDOMIZE value), and the program reads those bytes back with PEEK q / PEEK r to write the low and high bytes of each 16-bit address into the machine code. This avoids any direct address arithmetic in BASIC.
Memory Map
| Address | Purpose |
|---|---|
| 32500 | Output buffer — merged, ready-to-print letter |
| 37997 | Number of fields (byte) in data file header |
| 37998–37999 | Length of loaded CODE block (low/high byte) |
| 38000 | Letter template loaded from tape |
| 60000–60013 | Machine code LDIR routine |
| 61000 | Data file loaded from tape |
| 23670–23671 | System variable: last RANDOMIZE seed (used for address patching) |
Key BASIC Idioms and Techniques
- RANDOMIZE for address passing: Using
RANDOMIZEto store a value in a known system variable location and immediatelyPEEKing it back is an efficient way to split a 16-bit integer into low/high bytes without using INT or MOD arithmetic. - Word-wrap in lines 3060–3110: The renderer scans forward up to 32 characters, tracks the last space position, and breaks there, implementing a rudimentary word-wrap without string slicing beyond what the BASIC
b$(TO y-x)in line 20 provides. - Field array
b(): Rather than storing field content,b()stores the memory addresses of each field’s start within the data file at 38000, enabling direct PEEK-based copying in line 2030 without intermediate string allocation. - Printer detection (line 1040): Reads port 251 (IN 251) to test keyboard row bits — value 58 indicates the printer is present/selected, value 126 the normal keyboard state. The expression using
(1 AND ...)and(0 AND ...)produces a boolean 0/1 result forp. - Pad-to-10 loop (lines 70–71): Trims silently if too long, then pads with spaces in a tight self-referencing
GO TOloop untilz$is exactly 10 characters, matching the tape filename length requirement.
Special Control Bytes in Templates
131— Field delimiter in the data record; marks boundaries between fields.139— Paragraph/section break marker in the letter template (triggers top-of-column spacing).143— End-of-letter sentinel; written at the tail of the loaded CODE block (line 1030) and tested in line 1140 to detect end of records.144— Special character in the output stream (tested at line 3030); causes the renderer to fall through to the word-wrap block rather than treating it as a line-break control.134— Form-feed / page-eject marker (line 3300); triggers a fullCOPYand advances the record pointer.94(caret^) — Field substitution marker in the template; introduces a numeric field index to be looked up inb().
Bugs and Anomalies
- Line 3110: The line ends with
LET x=x-1:followed by a bare colon — this trailing colon is a no-op but syntactically harmless. - Line 3220: Entirely commented out with
REM, yet the code before it (lines 3210–3219) still performs partial spacing logic; the intent of the REM’d double-line spacing is unclear and may represent an unfinished edit. - Line 1040: The second term
(0 AND IN 251=126)always evaluates to 0 regardless of the port reading, sopcan only ever be 0 or 1 based solely on the first term. The second term appears to be a leftover from a more complex detection attempt. - Line 70: If
LEN z$ > 10,b$is set to" "(a space), but the condition check at line 1010 testsb$<>"c", so an over-length name silently re-prompts — this is functional but the logic is indirect.
Content
Source Code
10 LET q=23670: LET r=23671: RESTORE : FOR j=60000 TO 60013: READ k: POKE j,k: NEXT j: GO TO 1000
11 DATA 1,0,0,33,0,0,84,93,33,0,0,237,176,201
20 PRINT b$( TO y-x): LET x=y+1
21 IF x1=22 AND p=1 THEN COPY : CLS : LET x1=0
22 IF x1=22 AND p=0 THEN INPUT c$: CLS : LET x1=0
23 LET x1=x1+1: LET x4=x4+1: RETURN
30 RANDOMIZE v1: POKE 60001,PEEK q: POKE 60002,PEEK r: RANDOMIZE v2: POKE 60004,PEEK q: POKE 60005,PEEK r: RANDOMIZE v3: POKE 60009,PEEK q: POKE 60010,PEEK r: RANDOMIZE USR 60000: RETURN
70 IF LEN z$>10 THEN LET b$=" "
71 IF LEN z$<>10 THEN LET z$=z$+" ": GO TO 71
72 INPUT "Key C if OK ";b$: RETURN
1000 CLS : PRINT TAB 10;"MERGE 1"
1010 PRINT ''"Type the letter name ": INPUT z$: PRINT ''z$: GO SUB 70: IF b$<>"c" OR z$="" THEN GO TO 1
1020 PRINT ''"Start the tape": LOAD z$CODE : CLS
1030 LET c1=PEEK 37998+256*PEEK 37999: POKE 37999+c1,143: IF c1>4500 THEN PRINT "Letter too long": PAUSE 250: GO TO 1
1040 LET p=(1 AND IN 251=58)+(0 AND IN 251=126)
1060 LET x3=80: IF p=1 THEN PRINT "Printer ON. How many lines/page?": INPUT x3
1070 LET v1=c1: LET v2=61000: LET v3=38000: GO SUB 30
1100 PRINT "Type the file name ": INPUT z$: PRINT ''z$: GO SUB 70: IF b$<>"c" OR z$="" THEN GO TO 1070
1110 PRINT ''"Start the tape": LOAD z$CODE : CLS
1120 LET c5=PEEK 37998+256*PEEK 37999: LET a=PEEK 37997
1130 LET z=38000
1140 CLS : PRINT FLASH 1;"Building the next letter": DIM b(a+1): IF PEEK z=143 THEN GO TO 5000
1150 LET b(1)=z: FOR j=1 TO a
1160 IF PEEK z=131 THEN LET b(j+1)=z+1: GO TO 1180
1170 LET z=z+1: GO TO 1160
1180 LET z=z+1: NEXT j
1190 PRINT "Fields found"
1200 LET w=61000: LET x=32500
1210 IF PEEK w=94 THEN GO TO 2000
1220 POKE x,PEEK w: LET x=x+1: LET w=w+1: IF w=61000+c1 THEN CLS : GO SUB 3000: LET w=61000: LET z=z+1: GO TO 1140
1240 GO TO 1210
2000 LET w=w+1: LET b$=""
2010 IF PEEK w=94 OR PEEK w=44 THEN GO TO 2030
2020 LET b$=b$+CHR$ PEEK w: LET w=w+1: GO TO 2010
2030 LET f=VAL b$: FOR j=b(f) TO b(f+1)-2: POKE x,PEEK j: LET x=x+1: NEXT j
2040 IF PEEK w=94 THEN LET w=w+1: LET x=x-1: GO TO 1210
2050 GO TO 2000
3000 LET x=32500
3010 LET x1=1: LET x2=x1: LET x4=x1
3020 LET y=31
3030 LET f=PEEK x: IF f=144 THEN GO TO 3050
3035 IF f=42 THEN GO TO 4000
3040 IF f>127 THEN LET x=x+1: IF f<>131 THEN GO TO 3200
3050 IF INKEY$="z" THEN GO TO 1
3060 LET f=0: LET b$="": FOR j=x TO x+y: LET b$=b$+CHR$ PEEK j: IF (PEEK j>127 AND PEEK j<>144) AND f=0 THEN LET f=j
3070 NEXT j: LET y=j: IF f>0 THEN LET y=f
3080 LET f=0: FOR j=x TO y: IF PEEK j=32 THEN LET f=1: LET j=y
3090 NEXT j: IF f=0 THEN GO TO 3110
3100 IF PEEK y<>32 AND PEEK y<128 THEN LET y=y-1: GO TO 3100
3110 GO SUB 20: IF PEEK y>127 THEN LET x=x-1:
3120 GO SUB 3400: GO TO 3020
3200 IF f<>139 THEN GO TO 3300
3210 LET k=x3-x4: IF k<3 THEN FOR j=1 TO k: PRINT : GO SUB 21: GO SUB 3400: NEXT j: GO TO 3230
3220 REM PRINT : GO SUB 21: GO SUB 3400: PRINT : GO SUB 21: GO SUB 3400
3230 PRINT TAB 5;: LET y=26: GO TO 3030
3300 IF f<>134 THEN GO TO 3430
3310 LET k=x3-x4: PRINT : GO SUB 21: FOR j=1 TO k: PRINT : GO SUB 21: NEXT j: GO SUB 3410
3320 GO TO 3020
3400 IF x4<x3 OR p=0 THEN RETURN
3410 FOR j=1 TO 5: PRINT : GO SUB 21: IF j=3 THEN PRINT TAB 15;x2: LET x2=x2+1: GO SUB 21
3420 NEXT j: LET x4=1: RETURN
3430 IF p=0 THEN FOR j=1 TO 5: PRINT : NEXT j
3440 IF p=1 THEN COPY
3450 LET z=z+1: GO TO 1140
5000 CLS : PRINT AT 9,9;"End of list"'''"Key C for another copy": INPUT b$: IF b$="c" THEN GO TO 1120
5999 STOP
6000 SAVE "Merge 1" LINE 1
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
