Skip to content

Latest commit

 

History

History
201 lines (144 loc) · 6.8 KB

File metadata and controls

201 lines (144 loc) · 6.8 KB

Standalone CONVERTOR

Purpose

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.

Recommended reading model

Read the controller under src/utilities/convertor/controller/ in this order:

  1. the outer interpreter and four document representations;
  2. in-place relocation and repeat expansion;
  3. backwards DESKTOP row serialization and checksum generation;
  4. the Save/Verify/Reload conversation;
  5. self-modifying transfer and translation primitives;
  6. the inline filename editor and threaded strings;
  7. 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.

Central model

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.

Executable state

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.

Register narratives

The new source makes four otherwise confusing register conventions explicit.

Carry

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.

EXX

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

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 stack

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.

Threaded filename editor

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.

ROM inner-tape ABI

transfer_tape_block calls the ROM inner entry at $0562, not the public wrapper. The three instructions

        inc     d
        ex      af,af'
        dec     d

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

Conversion formats

The literate source keeps the evidence boundary explicit:

  • length $005E selects a descriptor whose first two words give the size of a following tape block;
  • header parameter TP removes a fixed $038E-byte prefix;
  • header parameter XT selects the common variable-line converter;
  • controls below space are discarded;
  • $BF-$FF encode byte-$BE spaces;
  • CR pads the current row to 64 columns;
  • $A5+n later emits n+1 additional copies of the previous output byte.

The code does not claim which commercial editor corresponds to every branch until representative fixtures prove that mapping.

Checksum trick

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.

Source and verification

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.py
protected BASIC payload: 7,276 bytes
SHA-256: 680f46203f4e4b5abe7f9d5f675e133a36d7bedea6a3aabea694d40757dea875

expanded runtime:       41,536 bytes
SHA-256: 93455bd4db80d545edcc0e9075ade17453afb5cf263ec44debc9f3ea2c4eeb0f