--- title: "Merge 1" id: 55947 type: "computer_media" slug: "merge-1" url: "http://localhost/computer_media/merge-1/" markdown_url: "http://localhost/computer_media/merge-1.md" published_at: "2024-07-14T09:01:12+00:00" modified_at: "2026-03-30T21:44:04+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2024/07/SCR-20240714-fcih.png" excerpt: "A tape-based mail-merge utility uses an embedded machine code block-copy routine to assemble personalized letters from a template and data file." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" genre: - name: "Business" slug: "business" taxonomy: "genre" url: "http://localhost/type/business/" media_contents: - id: 55923 title: "Timex Sinclair Public Domain Library Tape 2003" type: "computer_media" url: "http://localhost/computer_media/timex-sinclair-public-domain-library-tape-2003/" - id: 54965 title: "ISTUG Public Domain Library 5" type: "computer_media" url: "http://localhost/computer_media/istug-public-domain-library-5/" media_type: "Program" download_url: "https://archive.org/download/timex-sinclair-software-archive/Merge1%20%28198x%29%28TS2068%29%28US%29%28Program%29.zip" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2024/07/SCR-20240714-fcih.png" related_content: - id: 54570 title: "FILE 1" type: "computer_media" url: "http://localhost/computer_media/file-1/" media_type_tags: "Business" --- # Merge 1 With [File 1](http://localhost/computer_media/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:**`SAVE` line 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 `RANDOMIZE` to store a value in a known system variable location and immediately `PEEK`ing 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 for `p`. - **Pad-to-10 loop (lines 70–71):** Trims silently if too long, then pads with spaces in a tight self-referencing `GO TO` loop until `z$` 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 full `COPY` and advances the record pointer. - `94` (caret `^`) — Field substitution marker in the template; introduces a numeric field index to be looked up in `b()`. ### 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, so `p` can 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 tests `b$<>"c"`, so an over-length name silently re-prompts — this is functional but the logic is indirect. ## 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