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 a block of data ready to be punched to tape such that the
65/// standard reader leader can load it.
66///
67/// See reaaderleader.rs for documentation on the format of a block.
68///
69/// For the last block, the jump address is 0o26, which is the
70/// location within the reader leader which arranges to start the
71/// user's program.  For other blocks it is 3 (that is, we jump back
72/// to the start of the reader leader in order to load the next
73/// block).
74fn create_tape_block(
75    address: Address,
76    code: &[Unsigned36Bit],
77    last: bool,
78) -> Result<Vec<Unsigned36Bit>, AssemblerFailure> {
79    let length = code.len();
80    if code.is_empty() {
81        return Err(AssemblerFailure::BadTapeBlock {
82            address,
83            length,
84            msg: "tape block is empty but tape blocks are not allowed to be empty (the format does not support it)".to_string()
85        });
86    }
87    let len: Unsigned18Bit = match Unsigned18Bit::try_from(code.len()) {
88        Err(_) => {
89            return Err(AssemblerFailure::BadTapeBlock {
90                address,
91                length,
92                msg: "block is too long for output format".to_string(),
93            });
94        }
95        Ok(len) => len,
96    };
97    let end: Unsigned18Bit = match Unsigned18Bit::from(address)
98        .checked_add(len)
99        .and_then(|n| n.checked_sub(Unsigned18Bit::ONE))
100    {
101        None => {
102            return Err(AssemblerFailure::BadTapeBlock {
103                address,
104                length,
105                msg: "end of block does not fit into physical memory".to_string(),
106            });
107        }
108        Some(end) => end,
109    };
110    event!(
111        Level::DEBUG,
112        "creating a tape block with origin={address:>06o}, len={len:o}, end={end:>06o}"
113    );
114    let mut block = Vec::with_capacity(code.len().saturating_add(2usize));
115    let encoded_len: Unsigned18Bit = match Signed18Bit::try_from(len) {
116        Ok(n) => Signed18Bit::ONE.checked_sub(n),
117        Err(_) => None,
118    }
119    .expect("overflow in length encoding")
120    .reinterpret_as_unsigned();
121    let mut checksum = Signed18Bit::ZERO;
122    block.push(join_halves(encoded_len, end));
123    block.extend(code);
124
125    for w in &block {
126        checksum = update_checksum(checksum, *w);
127    }
128    let next: Unsigned18Bit = { if last { 0o27_u8 } else { 0o3_u8 }.into() };
129    checksum = update_checksum_by_halfword(checksum, next.reinterpret_as_signed());
130    let balance = Signed18Bit::ZERO.wrapping_sub(checksum);
131    checksum = update_checksum_by_halfword(checksum, balance);
132    block.push(join_halves(balance.reinterpret_as_unsigned(), next));
133    assert_eq!(checksum, Signed18Bit::ZERO);
134    Ok(block)
135}
136
137/// Assemble a single instruction to go into register 0o27,
138/// immediately after the reader leader.  This instruction calls the
139/// user's program.
140fn create_begin_block(
141    program_start: Option<Address>,
142    empty_program: bool,
143) -> Result<Vec<Unsigned36Bit>, AssemblerFailure> {
144    let disconnect_tape = SymbolicInstruction {
145        // 027: ¹IOS₅₂ 20000     ** Disconnect PETR, load report word into E.
146        held: false,
147        configuration: u5!(1), // signals that PETR report word should be loaded into E
148        opcode: Opcode::Opr,
149        index: u6!(0o52),
150        operand_address: OperandAddress::direct(Address::from(u18!(0o020_000))),
151    };
152    let jump: SymbolicInstruction = if let Some(start) = program_start {
153        let (physical, deferred) = start.split();
154        if deferred {
155            // If we simply passed though the defer bit, everything
156            // would likely work.  It's just that for this case we
157            // should closely examine what appear to be the original
158            // (TX-2 assembly language) programmer's assumptions about
159            // what will happen.
160            unimplemented!(
161                "PUNCH directive specifies deferred start address {start:o}; this is (deliberately) not yet supported - check carefully!"
162            );
163        }
164        // When there is a known start address `start` we emit a `JPQ
165        // start` instruction into memory register 0o27.
166        SymbolicInstruction {
167            held: false,
168            configuration: u5!(0o14), // JPQ
169            opcode: Opcode::Jmp,
170            index: Unsigned6Bit::ZERO,
171            operand_address: OperandAddress::direct(Address::from(physical)),
172        }
173    } else {
174        // Emit a JPD (jump, dismiss) instruction which loops back to
175        // itself.  This puts the machine into the LIMBO state.
176        SymbolicInstruction {
177            held: false,
178            configuration: u5!(0o20), // JPD
179            opcode: Opcode::Jmp,
180            index: Unsigned6Bit::ZERO,
181            operand_address: OperandAddress::direct(Address::from(u18!(0o27))),
182        }
183    };
184    let location = Address::from(Unsigned18Bit::from(0o27_u8));
185    let code = vec![
186        Instruction::from(&disconnect_tape).bits(),
187        Instruction::from(&jump).bits(),
188    ];
189    create_tape_block(location, &code, !empty_program)
190}
191
192/// Write the user's program as a tape image file.
193///
194/// # Errors
195///
196/// - A tape block is longer than will fit into an
197///   18-bit offset value
198/// - The program is larger than the TX-2's physical
199///   memory
200/// - A block's origin and length mean that it would
201///   extend past the end of ther TX-2's physical
202///   memory.
203/// - Failure to write the output file.
204pub fn write_user_program<W: Write>(
205    binary: &Binary,
206    writer: &mut W,
207    output_file_name: &Path,
208) -> Result<(), AssemblerFailure> {
209    let span = span!(Level::ERROR, "write binary program");
210    let _enter = span.enter();
211
212    // The boot code reads the paper tape in PETR mode 0o30106
213    // (see base/src/memory.rs) which looks for an END MARK
214    // (code 0o76, no seventh bit set).  But, our PETR device
215    // emulation currently "invents" the END MARK (coinciding
216    // with the beginnng of the tape file) so we don't need to
217    // write it.
218    write_data(writer, output_file_name, &reader_leader())?;
219    for chunk in binary.chunks() {
220        if chunk.is_empty() {
221            event!(
222                Level::ERROR,
223                "Will not write empty block at {:o}; the assembler should not have generated one; this is a bug.",
224                chunk.address,
225            );
226            continue;
227        }
228        let block = create_tape_block(chunk.address, &chunk.words, false)?;
229        write_data(writer, output_file_name, &block)?;
230    }
231
232    // After the rest of the program is punched, we write the special
233    // block for register 27.  This has to be last, becaause the
234    // standard reader leader uses the "next" field of the header to
235    // determine which is the last block.  When the "next" field
236    // points at 0o27 instead of 0o3, that means this is the final
237    // block.  So we have to emit this one last.
238    write_data(
239        writer,
240        output_file_name,
241        &create_begin_block(binary.entry_point(), binary.is_empty())?,
242    )?;
243
244    writer.flush().map_err(|e| {
245        AssemblerFailure::Io(IoFailed {
246            action: IoAction::Write,
247            target: IoTarget::File(output_file_name.to_owned()),
248            error: e,
249        })
250    })
251}