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}