assembler/
readerleader.rs

1//! The first part of the binary program output.
2//!
3//! The Reader Leader is a short piece of code which reads the rest of
4//! the tape and loads the program into memory at the spevcified
5//! locations.
6use base::instruction::{Instruction, Opcode, OperandAddress, SymbolicInstruction};
7use base::prelude::*;
8
9/// Convert a bit designator (as described in the documentation for
10/// the SKM opcode on [page 3-34 of the User
11/// Handbook](https://archive.org/details/tx-2-users-handbook-nov-63/page/n35/mode/1up))
12/// into an `Unsigned6Bit` field (suitable for use as the index portion
13/// of an instruction word).
14fn bit_index(q: u8, bitnum: u8) -> Unsigned6Bit {
15    let quarter = match q {
16        1..=3 => q,
17        4 => 0,
18        _ => {
19            panic!("invalid quarter number {q}");
20        }
21    };
22    assert!(bitnum <= 12, "invalid bit number {bitnum}");
23    Unsigned6Bit::try_from((quarter << 4) | bitnum).unwrap()
24}
25
26#[test]
27fn test_bit_index() {
28    assert_eq!(
29        bit_index(4, 12),
30        Unsigned6Bit::try_from(12).expect("test data should be valid")
31    );
32}
33
34/// Returns the standard reader leader.
35///
36/// The listing for this is given on [page 5-26 of the User
37/// Handbook](https://archive.org/details/tx-2-users-handbook-nov-63/page/n150/mode/1up).
38///
39/// This program is superficially similar to Program VI ("A Binary
40/// Read-In Routine") in [Lincoln Lab Memorandum 6M-5780 ("Some
41/// Examples of TX-2
42/// Programming")](http://www.bitsavers.org/pdf/mit/tx-2/6M-5780_Some_Examples_of_TX-2_Programming_Jul1958.pdf),
43/// but it is different in detail.
44///
45/// ## Disassembly
46///
47/// Here's a disassembly of the reader leader:
48///
49/// <pre>
50/// Loc  Symbolic assembly
51/// 00                   ** Used as a temporary for words read from tape
52/// 01                   ** unused?
53/// 02                   ** unused?
54///
55/// 03   ¹RSX₅₄ 5        ** set X₅₄=-5
56/// 04  ³⁶JMP₅₄ 20       ** Call procedure to read first word into [0]
57/// 05 h ²RSX₅₃ 0        ** Set X₅₃ = L([0])  ([0] is saved in E)
58/// 06   ¹STE 11         ** Set R([11]) to the word we read from tape.
59///
60/// 07  ³⁶JMP₅₄ 17       ** Call procedure at 17 (clear metabit, read word)
61/// 10 h  LDE   0        ** Load new word into E.
62///
63///                      ** R([11]) is the end address of the current
64///                      ** block (this insruction is modified by the
65///                      ** instruction at 06).
66/// 11    STE₅₃ 34       ** Store new word at [X₅₃+end]
67/// 12 h ¹JNX₅₃ 7        ** Loop to 7 when X₅₃<0. Postincrement X₅₃.
68/// 13  ³⁶JMP₅₄ 20       ** Call procedure to read another word into [0]
69/// 14 h  JPX₅₆ 377760   ** if X₅₆ > 0, restart tape loading
70/// 15 h  JNX₅₆ 377760   ** if X₅₆ < 0, restart tape loading
71/// 16  ¹⁴JPQ 27         ** Call user program (the instruction at 25 may have
72///                      ** changed this address).
73///
74/// ** Read-word procedure (entry point 1, clears meta bit)
75/// 17   ²MKZ₄.₁₂ 400011 ** Reset meta bit of [11]
76/// ** Read-word procedure (entry point 2, meta bit unaffected)
77/// ** On entry , X₅₄ is the return address.
78/// 20   ¹RSX₅₇ 3        ** Set R(X₅₇)=R(3) which is 5.
79/// 21 h  TSD 0          ** Read tape bits into [0].
80/// 22  ³⁶JPX₅₇ 21       ** Loop (to TSD) when X₅₇>0 (i.e. do whole word)
81/// 23   ¹AUX₅₆ 0        ** Add R[0] to X₅₆
82/// 24 h ²AUX₅₆ 0        ** Add L[0] to X₅₆
83/// 25   ¹STE 16         ** Set R[16] to E (which we loaded from [0]).
84/// 26  ¹⁵BPQ₅₄ 0        ** Branch to X₅₄ (no offset) - in other words, return.
85///
86/// Location 26 is the last word of the standard reader leader as
87/// loaded by the boot code, but the assembler includes the
88/// instructions after it in a special block loaded at location 27:
89///
90/// 27 ¹IOS₅₂ 20000    ** Disconnect PETR, load report word into E.
91///                    ** This instruction is replaced by `JPQ AA` when
92///                    ** a start address is used with ☛☛PUNCH.
93/// </pre>
94///
95/// ## Input Format
96///
97/// I believe the input format expected by the standard reader leader
98/// is:
99///
100/// <pre>
101/// header word: len,,end
102/// The header is followed by (1-len) words of body
103/// trailer word: sum,,next
104/// </pre>
105///
106/// All blocks start with `len,,end` where `len` is one plus the
107/// negated value of the actual length of the block in words (not
108/// including first and last).  end is the address of the last word
109/// which should be written by this block.
110///
111/// Neither the header word nor the trailer word is written into the
112/// memory area identified by `end`.
113///
114/// The checksum calculation carried out in X₅₆ computes the unsigned
115/// 18-bit total of left and right halves of all words in the block
116/// (including the header and trailer words).  This checksum must come
117/// out to 0, otherwise the reader leader will jump to 377760 to
118/// restart the loading process (which will re-load the bin,
119/// i.e. rewind the tape).
120///
121/// ## First, Middle and Last Blocks
122///
123/// All blocks except the last block should be created and are used in
124/// the same way.  The last block is different from the others simply
125/// because it has a different value of `next`.
126///
127/// The first block doesn't have to do anything in particular.  All
128/// blocks except the last should end with `checksum,,3` in order to
129/// make the reader leader read the next block.  The last block ends
130/// with `checksum,,AA` where `AA` is the execution start address of
131/// the user's program.
132///
133/// ## Minimal Example
134///
135/// I believe that the minimal tape content after the reader leader is
136/// a block containing a single word.  This example shows such a block
137/// loaded at address 0o20000:
138///
139/// <pre>
140/// 0,,20000
141/// 0
142/// 740000,,20000
143/// </pre>
144///
145/// Here the single word of the block (0) is loaded at 20000.  The
146/// execution address is also 20000.  The instruction at 20000 is 0.
147/// Since 0 is not a valid opcode, this program will fail. and the
148/// TX-2 will raise the illegal instruction (OCSAL) alarm.
149///
150/// The checksum value 0o740000 ensures that the total of all four
151/// halfwords is 0 (modulo 2^18).
152///
153/// ## Start Address
154///
155/// Notice that the disassembly above shows that address 27 contains
156/// `¹IOS₅₂` 20000.  This is taken from the listing on [page 5-26 of
157/// the Users
158/// Handbook](https://archive.org/details/tx-2-users-handbook-nov-63/page/n150/mode/1up).
159/// That apparently contraditcs the commentary on [page 6-23 of the
160/// same
161/// document](https://archive.org/details/tx-2-users-handbook-nov-63/page/n175/mode/1up),
162/// which states that the `IOS` instruction is at location 26.
163/// However, this difference is not material.
164///
165/// If we follow the advice given on [page
166/// 6-23](https://archive.org/details/tx-2-users-handbook-nov-63/page/n175/mode/1up)
167/// for the last word of a block, we would set it to checksum,,26
168/// meaning that the reader leader at locaiton 16 will jump to
169/// location 26.  The instruction at 26 (which is `¹⁵BPQ₅₄ 0`) will
170/// jump to the location in X₅₄.  That will (I think) have been set to
171/// 27 by the previous execution of `¹⁵BPQ₅₄ 0`.  So jump to location
172/// 26 has the effect of jumping to location 27 but also sets X₅₄
173/// (again) to 27.  This seems indistinguishable from setting the
174/// R(last) to 27, because in that case we begin execution at 27 with
175/// X₅₄ set to 27.
176///
177/// When the execution address of the last block is not either 26 or
178/// 27, the user's program will need to disconnect the paper tape
179/// reader if it doesn't require it.  This conclusion appears to
180/// contradict the guidance on [page
181/// 6-23](https://archive.org/details/tx-2-users-handbook-nov-63/page/n175/mode/1up). The
182/// apparent contradition would be resolved if it were the case the M4
183/// assembler adds a special first block containing a jump at location
184/// 28, when the `☛☛PUNCH` directive includes a start address.  This
185/// may in fact be the case shown in the diagram on [page
186/// 6-23](https://archive.org/details/tx-2-users-handbook-nov-63/page/n175/mode/1up).
187#[must_use]
188pub fn reader_leader() -> Vec<Unsigned36Bit> {
189    ([
190        // These instructions are taken from the middle column of
191        // page 5-26 of the Users Handbook.
192        //
193        // They are called by the boot code (the routine starting
194        // at 0o377760, see listing in section 5-5.2 of the Users
195        // Handbook).  The active sequence is 0o52, with X₅₂ =
196        // 0o377763, X₅₃ = 0, X₅₄ = 0.
197        SymbolicInstruction {
198            // 003: ¹RSX₅₄ 5   ** set X₅₄=-5
199            held: false,
200            configuration: u5!(1),
201            opcode: Opcode::Rsx,
202            index: u6!(0o54),
203            operand_address: OperandAddress::direct(Address::from(u18!(0o05))),
204        },
205        SymbolicInstruction {
206            // 004: ³⁶JMP₅₄ 20
207            //
208            // Save return address (which is 0o5) in X₅₄ and in R(E)
209            // and last memory reference in L(E), dismiss (lower the
210            // flag of sequence 52).  I believe that even though we
211            // dismiss sequence 52, there is no other runnable
212            // sequence, so execution continues.
213            //
214            // The called procedure reads the block header word from
215            // tape into [0], updates the checksum in X₅₆, and copies
216            // the right half of the loaded word into R[16].  E is
217            // overwritten.  The block header word is (1-len),,end as
218            // described in the comment above.  The fact that we load
219            // end into R[16] isn't important; we load R[word] into
220            // R[16] for every word we read (the right half of the
221            // last word in the block is the "next" address).
222            held: false,
223            configuration: u5!(0o36), // binary 011110
224            opcode: Opcode::Jmp,
225            index: u6!(0o54),
226            operand_address: OperandAddress::direct(Address::from(u18!(0o20))),
227        },
228        SymbolicInstruction {
229            // 005: h²RSX₅₃ 0      ** Set X₅₃ = L([0])  ([0] is saved in E)
230            //
231            // L[0] was read (by the call to 0o20 just above) from the
232            // first word of the tape block. X₅₃ holds (1-n) where n
233            // is the number of words still to be loaded for this
234            // block.
235            held: true,
236            configuration: u5!(2),
237            opcode: Opcode::Rsx,
238            index: u6!(0o53),
239            operand_address: OperandAddress::direct(Address::ZERO),
240        },
241        SymbolicInstruction {
242            // 006: ¹STE 11        ** Set R([11]) to the right half of the word we read from tape.
243            //                     ** That's the block's end address.
244            held: false,
245            configuration: u5!(1),
246            opcode: Opcode::Ste,
247            index: u6!(0),
248            operand_address: OperandAddress::direct(Address::from(u18!(0o11))),
249        },
250        SymbolicInstruction {
251            // 007: : ³⁶JMP₅₄ 17  ** Call procedure at 17
252            /* Saves return address (which is 0o10) in X₅₄ and in
253                   R(E) and last memory reference in L(E), dismiss
254                   (lower the flag of sequence 52 - but this has
255                   no effect since this already happened the first
256                   time we executed the instruction at 004).  I
257                   believe that even though we dismiss sequence
258                   52, there is no other runnable sequence, so
259                   execution continues.
260            */
261            held: false,
262            configuration: u5!(0o36),
263            opcode: Opcode::Jmp,
264            index: u6!(0o54),
265            operand_address: OperandAddress::direct(Address::from(u18!(0o17))),
266        },
267        /* On return from the procedure at 0o17, [0] contains the
268         * word we read. */
269        SymbolicInstruction {
270            // 010: h LDE 0        ** Load new word into E.
271            held: true,
272            configuration: u5!(0),
273            opcode: Opcode::Lde,
274            index: u6!(0),
275            operand_address: OperandAddress::direct(Address::ZERO),
276        },
277        SymbolicInstruction {
278            // 011: STE₅₃ 34       ** Store new word at [X₅₃+34]
279            /* X₅₃ was initialised to the LHS of the first word in the
280             * block and is incremented by the JNX instruction at the
281             * next location, 0o12.  The 034 here (being the right
282             * half of this instruction) is updated by the instruction
283             * at 006 to be the right half of the word we read from
284             * tape.
285             */
286            held: false,
287            configuration: u5!(0),
288            opcode: Opcode::Ste,
289            index: u6!(0o53),
290            operand_address: OperandAddress::direct(Address::from(u18!(0o34))),
291        },
292        SymbolicInstruction {
293            // 012: h¹JNX₅₃ 7     ** Loop to 7 when X₅₃<0. Postincrement X₅₃.
294            held: true,
295            configuration: u5!(1),
296            opcode: Opcode::Jnx,
297            index: u6!(0o53),
298            operand_address: OperandAddress::direct(Address::from(u18!(0o07))),
299        },
300        SymbolicInstruction {
301            // 013: ³⁶JMP₅₄ 20     ** Call procedure to read another word into [0]
302            held: false,
303            configuration: u5!(0o36),
304            opcode: Opcode::Jmp,
305            index: u6!(0o54),
306            operand_address: OperandAddress::direct(Address::from(u18!(0o20))),
307        },
308        SymbolicInstruction {
309            // 014: hJPX₅₆ 377760 ** if X₅₆ > 0, restart tape loading
310            held: true,
311            configuration: u5!(0),
312            opcode: Opcode::Jpx,
313            index: u6!(0o56),
314            operand_address: OperandAddress::direct(Address::from(u18!(0o377_760))),
315        },
316        SymbolicInstruction {
317            // 015: hJNX₅₆ 377760 ** if X₅₆ < 0, restart tape loading
318            held: true,
319            configuration: u5!(0),
320            opcode: Opcode::Jnx,
321            index: u6!(0o56),
322            operand_address: OperandAddress::direct(Address::from(u18!(0o377_760))),
323        },
324        SymbolicInstruction {
325            // 016: ¹⁴JPQ 27
326            //
327            // We arrive at this location (from 15) if X₅₆ is zero
328            // - that is, if the checksum is correct.
329            //
330            // Jump to register 27, which holds another jump
331            // instruction; that jumps to the user's code entry
332            // point.
333            held: false,
334            configuration: u5!(0o14),
335            // ¹⁴JMP = JPQ, see page 3-31 of Users Handbook
336            opcode: Opcode::Jmp,
337            index: u6!(0o0),
338            operand_address: OperandAddress::direct(Address::from(u18!(0o27))),
339        },
340        SymbolicInstruction {
341            // 017: ²MKZ₄.₁₂ 400011     ** Reset meta bit of [11]
342            held: false,
343            configuration: u5!(0o2),
344            opcode: Opcode::Skm,       // ²SKM is Mkz (p 3-34) "make zero"
345            index: bit_index(4, 0o12), // 4.12
346            operand_address: OperandAddress::deferred(Address::from(u18!(0o011))),
347        },
348        /* At 0o20 we have a procedure which loads a word from
349         * tape, adds it to our running checksum and leaves the
350         * word at [0]. */
351        SymbolicInstruction {
352            // 020: ¹RSX₅₇ 3     ** Set R(X₅₇)=R(3) which is 5.
353            held: false,
354            configuration: u5!(0o1),
355            opcode: Opcode::Rsx,
356            index: u6!(0o57),
357            operand_address: OperandAddress::direct(Address::from(u18!(3))),
358        },
359        SymbolicInstruction {
360            // 021: hTSD 0        ** Read tape bits into [0].
361            held: true,
362            configuration: u5!(0), // ignored anyway in ASSEMBLY mode
363            opcode: Opcode::Tsd,
364            index: u6!(0),
365            operand_address: OperandAddress::direct(Address::ZERO),
366        },
367        SymbolicInstruction {
368            // 022: ³⁶JPX₅₇ 21     ** Loop (to TSD) when X₅₇>0 (i.e. do whole word)
369            held: false,
370            configuration: u5!(0o36),
371            opcode: Opcode::Jpx,
372            index: u6!(0o57),
373            operand_address: OperandAddress::direct(Address::from(u18!(0o21))),
374        },
375        SymbolicInstruction {
376            // 023: ¹AUX₅₆ 0        ** Add R[0] to X₅₆
377            held: false,
378            configuration: u5!(1),
379            opcode: Opcode::Aux,
380            index: u6!(0o56),
381            operand_address: OperandAddress::direct(Address::ZERO),
382        },
383        SymbolicInstruction {
384            // 024: h²AUX₅₆ 0        ** Add L[0] to X₅₆
385            //                       ** This also sets E to [0].
386            held: true,
387            configuration: u5!(2),
388            opcode: Opcode::Aux,
389            index: u6!(0o56),
390            operand_address: OperandAddress::direct(Address::ZERO),
391        },
392        SymbolicInstruction {
393            // 025: ¹STE 16         ** Set R[16] to E (which we loaded from [0]).
394            held: false,
395            configuration: u5!(1),
396            opcode: Opcode::Ste,
397            index: u6!(0),
398            operand_address: OperandAddress::direct(Address::from(u18!(0o16))),
399        },
400        SymbolicInstruction {
401            // 026: ¹⁵BPQ₅₄ 0       ** Branch to X₅₄ (no offset)
402            //                      ** This is return from procedure call,
403            //                      ** e.g. from the call at 004.
404            //                      ** Overwrites E with saved return point, mem ref
405            held: false,
406            configuration: u5!(0o15),
407            opcode: Opcode::Jmp, // 0o05 is JMP (p 3-30); ¹⁵JMP = BPQ
408            index: u6!(0o54),
409            operand_address: OperandAddress::direct(Address::ZERO),
410        },
411        // Binaries have two insructions following this.  The first is
412        // `¹IOS₅₂ 20000` which therefore gets loaded at location 27.
413        // This is not included in the reader leader (as the last
414        // location the plugboard code loads is 0o26) but we know it
415        // needs to be here as the Users Handbook points out that the
416        // tape gets disconnected, and the instruction appears on page
417        // 5-26.
418        //
419        // The second instruction is a jump responsible for launching
420        // the user program.  The latter is added by the assembler
421        // (i.e. M4's PUNCH meta-command).
422    ])
423    .iter()
424    .map(|symbolic| -> Unsigned36Bit { Instruction::from(symbolic).bits() })
425    .collect()
426}
427
428#[test]
429fn test_reader_leader() {
430    let leader = reader_leader();
431    let expected: &[u64] = &[
432        // These values are taken from the right-hand column of page
433        // 5-26 of the Users Handbook.
434        //
435        // That table shows the first three words, but if you look at
436        // the plugboard code at 03777760, it loads 0o25 words of
437        // reader leader ending at location 0o27.  So we start at the
438        // word for address 3.
439        //
440        // In our comments below "Position" describes a word's
441        // position within the tape and "Final address" describes the
442        // memory location it gets loaded to.  Each word will occupy
443        // six consecutive lines of the tape because like the rest of
444        // the binary, the leader is punched in splayed mode.
445        //
446        // Instruction (oct)  Position (oct) Final address (oct)
447        // temporary storage               -                   0
448        // apparently unused               -                   1
449        // apparently unused               -                   2
450        0o011_154_000_005, //              0                   3
451        0o360_554_000_020, //              1                   4
452        0o421_153_000_000, //              2                   5
453        0o013_000_000_011, //              3                   6
454        0o360_554_000_017, //              4                   7
455        0o402_000_000_000, //              5                  10
456        0o003_053_000_034, //              6                  11
457        0o410_753_000_007, //              7                  12
458        0o360_554_000_020, //             10                  13
459        0o400_656_377_760, //             11                  14
460        0o400_756_377_760, //             12                  15
461        0o140_500_000_027, //             13                  16
462        0o021_712_400_011, //             14                  17
463        0o011_157_000_003, //             15                  20
464        0o405_700_000_000, //             16                  21
465        0o360_657_000_021, //             17                  22
466        0o011_056_000_000, //             20                  23
467        0o421_056_000_000, //             21                  24
468        0o013_000_000_016, //             22                  25
469        0o150_554_000_000, //             23                  26
470    ];
471
472    // The final word 0o010_452_020_000 apears in the assembly listing
473    // on page 5-26 but it is not loaded by the boot code in the
474    // plugboard (the last memory address that code loads is 0o26).
475
476    assert_eq!(expected.len(), 0o24);
477    assert_eq!(leader.len(), expected.len());
478    for (i, expected_value) in expected.iter().copied().enumerate() {
479        assert_eq!(
480            leader[i],
481            expected_value,
482            concat!(
483                "Mismatch in reader leader ",
484                "at file position {:#3o} (final memory address {:#3o}): ",
485                "expected 0o{:012o}, got 0o{:012o}; ",
486                "got instruction disassembles to {:?}"
487            ),
488            i,
489            i + 3,
490            expected_value,
491            leader[i],
492            &SymbolicInstruction::try_from(&Instruction::from(leader[i])),
493        );
494    }
495}