CONVERTOR is a tape-to-DESKTOP document transducer. The repository contains both the protected BASIC payload and the complete expanded runtime. The controller is commented in a purpose-oriented literate style while retaining all observed quirks and malformed-input behavior.
Read the controller under src/utilities/convertor/controller/ in this order:
- the outer interpreter and four document representations;
- in-place relocation and repeat expansion;
- backwards DESKTOP row serialization and checksum generation;
- the Save/Verify/Reload conversation;
- self-modifying transfer and translation primitives;
- the inline filename editor and threaded strings;
- the ROM tape ABI and source-container normalizer.
This is also memory order, but the comments deliberately introduce each representation before explaining the Z80 trick that implements it.
CONVERTOR is a tape-to-document transducer:
source tape block
-> source-container normalization
-> zero-padded 64-column rows
-> repeat-marker expansion and character-map selection
-> backwards compact DESKTOP prefix
-> fixed DESKTOP document suffix
-> Spectrum ROM CODE block
The program never owns a second full-sized document buffer. Instead, it changes where the data lives before each potentially expanding transformation:
$61A8 [source][zero capacity]
trim and move upward with LDDR
$61A8 [free capacity ........][source] $E678
expand upward from $61A8
$61A8 [canonical 64-column rows][zero capacity]
read rows backward, write prefix backward
[prefix start .... compact rows .... $EA5F][$EA60 fixed suffix .... $FF0E]
Building the final prefix backward is the decisive design choice. Its length is unknown, while the fixed document suffix already occupies high memory. Once the last row has been emitted, the current low address is immediately suitable as the ROM SAVE start address; no final relocation is required.
Three pieces of state are held in instruction bytes:
translate_source_character_bias_operand
active address bias for the $80-$9E translation map
prepare_generated_document_transfer_destination_operand
first byte of the generated variable prefix
generated_row_terminator_operand
$80/$84 control byte for the row currently being serialized
The comments identify both the writer and reader of every patched operand. They also explain why ordinary memory variables would cost more bytes or require more register preservation.
The new source makes four otherwise confusing register conventions explicit.
Carry selects LOAD versus VERIFY on entry to the Spectrum ROM tape routine. The
same flag is also the boolean result of sample_space_key: set means SPACE is
released, clear means abort. Tape results are therefore moved through AF'
while the keyboard is sampled.
The source-container normalizer stores its optional high-character map pointer
in alternate DE. This leaves the main DE free as the canonical output
pointer. The original $6185 defect remains visible: the code computes
HL=map+character but loads A,(DE), so mapped high characters collapse to the
first map byte $93.
BC begins as a byte count for LDIR/LDDR. After those instructions reduce it
to zero, C is reused as the current output column. This reuse explains several
sequences that otherwise look as if the column counter were never initialized.
The 94-byte descriptor path loads a second tape block. It temporarily pops the
return address of normalize_source_container so the SPACE and tape-error paths
can jump directly out of the abandoned conversion. The return is pushed back
only after the second load succeeds. The literate source includes the stack
shape at each stage.
The ten-byte filename editor is an especially compact form of threaded code:
CALL print_inline_high_bit_string
[name0 ... name9][cursor cell][high-bit terminator]
CALL read_printable_key
The printer pops the CALL return as its string pointer and jumps to the byte after the high-bit terminator. Thus the filename field is simultaneously live mutable data and an inline argument between two instructions.
The cursor address is the operand of LD HL,$0000. DELETE uses another code/data
coincidence: at the left edge, HL-2 reaches $84, the low byte of the preceding
CALL $6084; its high bit acts as the boundary sentinel without a separate
constant.
transfer_tape_block calls the ROM inner entry at $0562, not the public
wrapper. The three instructions
inc d
ex af,af'
dec dreconstruct the register state expected by that entry: the requested block flag
and LOAD/VERIFY carry move to AF', DE is restored, and the main flags are
those produced by DEC D. This is described as a ROM ABI contract rather than
as arbitrary flag manipulation.
The literate source keeps the evidence boundary explicit:
- length
$005Eselects a descriptor whose first two words give the size of a following tape block; - header parameter
TPremoves a fixed$038E-byte prefix; - header parameter
XTselects the common variable-line converter; - controls below space are discarded;
$BF-$FFencodebyte-$BEspaces;- CR pads the current row to 64 columns;
$A5+nlater emitsn+1additional copies of the previous output byte.
The code does not claim which commercial editor corresponds to every branch until representative fixtures prove that mapping.
After serialization, the output header already contains:
total_length = $FF0F - prefix_start
Adding $EB50 modulo 65536 produces $EA5F-prefix_start, one less than the
inclusive prefix byte count. The checksum loop decrements this value until it
underflows to $FFFF; testing bit 7 of D recognizes that underflow. The source
now explains why this unusual loop includes the final byte instead of stopping
one byte early.
The final source is divided into protected stream data, first-stage decoder,
controller phases, runtime workspace and immutable document/font template data.
The evidence boundary remains explicit: the direct fixed-row route belongs to
the Tasword/D-Text/R-Text family, while the exact one-to-one mapping of the
94-byte descriptor, TP and XT routes is not asserted without representative
historical files.
Both historical representations are assembled and checked by:
python3 tools/verify.pyprotected BASIC payload: 7,276 bytes
SHA-256: 680f46203f4e4b5abe7f9d5f675e133a36d7bedea6a3aabea694d40757dea875
expanded runtime: 41,536 bytes
SHA-256: 93455bd4db80d545edcc0e9075ade17453afb5cf263ec44debc9f3ea2c4eeb0f