Skip to main content

assembler/driver/
output.rs

1//! Generate the output binary, as a tape image file.
2use std::io::Write;
3use std::path::Path;
4
5use tracing::{Level, event, span};
6
7use base::prelude::{
8    Address, Instruction, Opcode, OperandAddress, Signed18Bit, SymbolicInstruction, Unsigned6Bit,
9    Unsigned18Bit, Unsigned36Bit, join_halves, split_halves, u5, u6, u18, unsplay,
10};
11
12use super::super::readerleader::reader_leader;
13use super::super::types::{AssemblerFailure, IoAction, IoFailed, IoTarget};
14use super::Binary;
15
16/// Write a sequence of 36-bit words in splayed/assembly mode.
17///
18/// We write the output in splayed mode simply because
19///
20/// - this is how the standard reader leader program expects to read it
21/// - the plugboard read-in program expects to read the standard
22///   reader-leader itself (from the tape) in that format.
23///
24fn write_data<W: Write>(
25    writer: &mut W,
26    output_file_name: &Path,
27    data: &[Unsigned36Bit],
28) -> Result<(), AssemblerFailure> {
29    let mut inner = || -> Result<(), std::io::Error> {
30        const OUTPUT_CHUNK_SIZE: usize = 1024;
31        for chunk in data.chunks(OUTPUT_CHUNK_SIZE) {
32            let mut buf: Vec<u8> = Vec::with_capacity(chunk.len() * 6);
33            for word in chunk {
34                let unsplayed: [Unsigned6Bit; 6] = unsplay(*word);
35                buf.extend(unsplayed.into_iter().map(|u| u8::from(u) | (1 << 7)));
36            }
37            writer.write_all(&buf)?;
38        }
39        Ok(())
40    };
41    inner().map_err(|e| {
42        AssemblerFailure::Io(IoFailed {
43            action: IoAction::Write,
44            target: IoTarget::File(output_file_name.to_path_buf()),
45            error: e,
46        })
47    })
48}
49
50/// Update the checksum of an output block (incorporating an 18-bit value).
51fn update_checksum_by_halfword(sum: Signed18Bit, halfword: Signed18Bit) -> Signed18Bit {
52    sum.wrapping_add(halfword)
53}
54
55/// Update the checksum of an output block (incorporating a 36-bit value).
56fn update_checksum(sum: Signed18Bit, word: Unsigned36Bit) -> Signed18Bit {
57    let (l, r) = split_halves(word);
58    update_checksum_by_halfword(
59        update_checksum_by_halfword(l.reinterpret_as_signed(), sum),
60        r.reinterpret_as_signed(),
61    )
62}
63
64/// Create (and return) a block of data ready to be punched to tape
65/// such that the standard reader leader can load it.
66///
67/// See ../readerleader.rs for documentation on the format of a block.
68fn create_tape_block(
69    address: Address,
70    code: &[Unsigned36Bit],
71    last: bool,
72) -> Result<Vec<Unsigned36Bit>, AssemblerFailure> {
73    let length = code.len();
74    if code.is_empty() {
75        return Err(AssemblerFailure::BadTapeBlock {
76            address,
77            length,
78            msg: "tape block is empty but tape blocks are not allowed to be empty (the format does not support it)".to_string()
79        });
80    }
81    let len: Unsigned18Bit = match Unsigned18Bit::try_from(code.len()) {
82        Err(_) => {
83            return Err(AssemblerFailure::BadTapeBlock {
84                address,
85                length,
86                msg: "block is too long for output format".to_string(),
87            });
88        }
89        Ok(len) => len,
90    };
91    let end: Unsigned18Bit = match Unsigned18Bit::from(address)
92        .checked_add(len)
93        .and_then(|n| n.checked_sub(Unsigned18Bit::ONE))
94    {
95        None => {
96            return Err(AssemblerFailure::BadTapeBlock {
97                address,
98                length,
99                msg: "end of block does not fit into physical memory".to_string(),
100            });
101        }
102        Some(end) => end,
103    };
104    event!(
105        Level::DEBUG,
106        "creating a tape block with origin={address:>06o}, len={len:o}, end={end:>06o}"
107    );
108    let mut block = Vec::with_capacity(code.len().saturating_add(2usize));
109    let encoded_len: Unsigned18Bit = match Signed18Bit::try_from(len) {
110        Ok(n) => Signed18Bit::ONE.checked_sub(n),
111        Err(_) => None,
112    }
113    .expect("overflow in length encoding")
114    .reinterpret_as_unsigned();
115    let mut checksum = Signed18Bit::ZERO;
116    block.push(join_halves(encoded_len, end));
117    block.extend(code);
118
119    for w in &block {
120        checksum = update_checksum(checksum, *w);
121    }
122    let next: Unsigned18Bit = { if last { 0o27_u8 } else { 0o3_u8 } }.into();
123    checksum = update_checksum_by_halfword(checksum, next.reinterpret_as_signed());
124    let balance = Signed18Bit::ZERO.wrapping_sub(checksum);
125    checksum = update_checksum_by_halfword(checksum, balance);
126    block.push(join_halves(balance.reinterpret_as_unsigned(), next));
127    assert_eq!(checksum, Signed18Bit::ZERO);
128    Ok(block)
129}
130
131/// Assemble a pair of instructions to go into registers 0o27 and
132/// 0o28, immediately after the reader leader.  This instruction calls
133/// the user's program (which begins at the address specified by the
134/// PUNCH meta command).
135fn create_begin_block(
136    program_start: Option<Address>,
137    empty_program: bool,
138) -> Result<Vec<Unsigned36Bit>, AssemblerFailure> {
139    // Page 6-23 (dated October 1961) of the Users Handbook states
140    // that this instruction goes into register 26 (and that 27
141    // contains a JPQ or JPD instruction).  But on the immediately
142    // following page (6-24, same date) it says "The special block for
143    // register 27...".
144    //
145    // Page 5-26 (dated November 1963) gives a listing of the standard
146    // reader leader which shows the IOS instruction at location 27:
147    //
148    // 26    ¹⁵BPQ₅₄ 0
149    // 27     ¹IOS₅₂ 20000
150    //
151    // So, one explanation for the apparent inconsistency is that
152    // changes were made between October 1961 and November 1963.
153    let disconnect_tape = SymbolicInstruction {
154        // 027: ¹IOS₅₂ 20000     ** Disconnect PETR, load report word into E.
155        held: false,
156        configuration: u5!(1), // signals that PETR report word should be loaded into E
157        opcode: Opcode::Opr,
158        index: u6!(0o52),
159        operand_address: OperandAddress::direct(Address::from(u18!(0o020_000))),
160    };
161    let jump: SymbolicInstruction = if let Some(start) = program_start {
162        let (physical, deferred) = start.split();
163        if deferred {
164            // If we simply passed though the defer bit, everything
165            // would likely work.  It's just that for this case we
166            // should closely examine what appear to be the original
167            // (TX-2 assembly language) programmer's assumptions about
168            // what will happen.
169            unimplemented!(
170                "PUNCH directive specifies deferred start address {start:o}; this is (deliberately) not yet supported - check carefully!"
171            );
172        }
173        // When there is a known start address `start` we emit a `JPQ
174        // start` instruction into memory register 0o27.
175        SymbolicInstruction {
176            held: false,
177            configuration: u5!(0o14), // JPQ
178            opcode: Opcode::Jmp,
179            index: Unsigned6Bit::ZERO,
180            operand_address: OperandAddress::direct(Address::from(physical)),
181        }
182    } else {
183        // Emit a JPD (jump, dismiss) instruction which loops back to
184        // itself.  This puts the machine into the LIMBO state.
185        SymbolicInstruction {
186            held: false,
187            configuration: u5!(0o20), // JPD
188            opcode: Opcode::Jmp,
189            index: Unsigned6Bit::ZERO,
190            operand_address: OperandAddress::direct(Address::from(u18!(0o27))),
191        }
192    };
193    let location = Address::from(Unsigned18Bit::from(0o27_u8));
194    let code = vec![
195        Instruction::from(&disconnect_tape).bits(),
196        Instruction::from(&jump).bits(),
197    ];
198    create_tape_block(location, &code, !empty_program)
199}
200
201/// Write the user's program as a tape image file.
202///
203/// # Errors
204///
205/// - A tape block is longer than will fit into an
206///   18-bit offset value
207/// - The program is larger than the TX-2's physical
208///   memory
209/// - A block's origin and length mean that it would
210///   extend past the end of the TX-2's physical
211///   memory.
212/// - Failure to write the output file.
213pub fn write_user_program<W: Write>(
214    binary: &Binary,
215    writer: &mut W,
216    output_file_name: &Path,
217) -> Result<(), AssemblerFailure> {
218    let span = span!(Level::ERROR, "write binary program");
219    let _enter = span.enter();
220
221    // The boot code reads the paper tape in PETR mode 0o30106
222    // (see base/src/memory.rs) which looks for an END MARK
223    // (code 0o76, no seventh bit set).  But, our PETR device
224    // emulation currently "invents" the END MARK (coinciding
225    // with the beginnng of the tape file) so we don't need to
226    // write it.
227    write_data(writer, output_file_name, &reader_leader())?;
228
229    // Write the special block for register 27.
230    //
231    // We used to write this block after the final block of the user's
232    // program, so that we could signal that this special block was
233    // the final one.  But this is not correct, because the Users
234    // Handbook states on page 6-24 (in the description of the PUNCH
235    // meta command):
236    //
237    // Note also: The special block for register 27 comes before the
238    // program on the tape.  Program material that is to go in
239    // register 27 therefore supercedes this special block created by
240    // M4.
241    write_data(
242        writer,
243        output_file_name,
244        &create_begin_block(binary.entry_point(), binary.is_empty())?,
245    )?;
246
247    // Write the blocks of the user program.
248    let mut iter = binary.chunks().iter().peekable();
249    while let Some(chunk) = iter.next() {
250        if chunk.is_empty() {
251            event!(
252                Level::ERROR,
253                "Will not write empty block at {:o}; the assembler should not have generated one; this is a bug.",
254                chunk.address,
255            );
256            continue;
257        }
258        let last: bool = iter.peek().is_none();
259        let block = create_tape_block(chunk.address, &chunk.words, last)?;
260        write_data(writer, output_file_name, &block)?;
261    }
262
263    writer.flush().map_err(|e| {
264        AssemblerFailure::Io(IoFailed {
265            action: IoAction::Write,
266            target: IoTarget::File(output_file_name.to_owned()),
267            error: e,
268        })
269    })
270}