tx2maketape/
main.rs

1//! This program converts an octal dump of a program (as shown in the
2//! right-hand columns of an assembler listing) into a tape image
3//! suitable for loading into a simulated TX-2.
4use std::{
5    error::Error,
6    ffi::OsString,
7    fmt::Display,
8    fs::{File, OpenOptions},
9    io::{BufRead, BufReader, BufWriter},
10    path::PathBuf,
11};
12
13use clap::ArgAction::Set;
14use clap::Parser;
15use regex::Regex;
16
17use assembler::{Binary, BinaryChunk, write_user_program};
18use base::prelude::{Address, Unsigned18Bit, Unsigned36Bit, join_halves};
19
20const OCTAL: u32 = 8;
21
22#[derive(Debug)]
23struct Fail(String);
24
25impl Display for Fail {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        self.0.fmt(f)
28    }
29}
30impl Error for Fail {}
31
32// Thanks to Google for allowing this code to be open-sourced.  I
33// generally prefer to correspond about this project using my
34// personal email address rather than my work one, though.
35const AUTHOR: &str = "James Youngman <james@youngman.org>";
36
37/// Make a tape image for the historical TX-2 computer
38#[derive(Parser, Debug)]
39#[clap(author = AUTHOR, version, about, long_about = None)]
40struct Cli {
41    /// File from which the octal dump is read.
42    #[clap(action=Set)]
43    input: OsString,
44
45    /// File to which assembler output is written
46    #[clap(action = Set, short = 'o', long)]
47    output: OsString,
48}
49
50fn read_binary(input_file: File) -> Result<Binary, Fail> {
51    let pattern: &'static str = r"^\s*[|](\d+) +(\d+)[|]\s*(\d+)\s*$";
52    let line_rx = match Regex::new(pattern) {
53        Ok(rx) => rx,
54        Err(e) => {
55            return Err(Fail(format!(
56                "internal error: failed to compile regular expression '{pattern}': {e}"
57            )));
58        }
59    };
60    let reader = BufReader::new(input_file);
61
62    let mut binary: Binary = Default::default();
63    let mut chunk: Option<BinaryChunk> = None;
64    let mut prev_addr: Option<Unsigned18Bit> = None;
65
66    for line_result in reader.lines() {
67        let line = match line_result {
68            Err(e) => {
69                return Err(Fail(format!("failed to read from input file: {e}")));
70            }
71            Ok(line) => line,
72        };
73        let line = line.trim();
74        if line.is_empty() {
75            continue; // ignore blank lines.
76        }
77        let matches = match line_rx.captures(line) {
78            Some(m) => m,
79            None => {
80                return Err(Fail(format!(
81                    "input line '{line}' does not match required regular expression {pattern})"
82                )));
83            }
84        };
85        let (_, [left, right, address]) = matches.extract();
86        if left.len() != 6 {
87            return Err(Fail(format!(
88                "input line {line} has field {left} but it should be 6 characters long"
89            )));
90        }
91        let left: Unsigned18Bit = match u32::from_str_radix(left, OCTAL).map(|n| n.try_into()) {
92            Ok(Ok(n)) => n,
93            _ => {
94                return Err(Fail(format!("field {left} should be an octal number")));
95            }
96        };
97        if right.len() != 6 {
98            return Err(Fail(format!(
99                "input line {line} has field {right} but it should be 6 characters long"
100            )));
101        }
102        let right: Unsigned18Bit = match u32::from_str_radix(right, OCTAL).map(|n| n.try_into()) {
103            Ok(Ok(n)) => n,
104            _ => {
105                return Err(Fail(format!("field {right} should be an octal number")));
106            }
107        };
108        let prev_plus_one: Option<Unsigned18Bit> = match prev_addr {
109            None => None,
110            Some(p) => match p.checked_add(Unsigned18Bit::ONE) {
111                Some(n) => Some(n),
112                None => {
113                    // There could be an off-by-one error here, but
114                    // it's not important, because loading a binary
115                    // into V-memory (which is at the top) isn't
116                    // something you should expect to work.
117                    return Err(Fail("A block is too long to fit in memory".to_string()));
118                }
119            },
120        };
121
122        let word: Unsigned36Bit = join_halves(left, right);
123        let addr: Unsigned18Bit = match u32::from_str_radix(address, OCTAL).map(|n| n.try_into()) {
124            Ok(Ok(n)) => {
125                if address.len() == 3 {
126                    match prev_plus_one {
127                        Some(p) => (p & 0o777000) | (n & 0o777),
128                        None => {
129                            return Err(Fail(format!(
130                                "The first address field {address} should be 6 characters long"
131                            )));
132                        }
133                    }
134                } else {
135                    n
136                }
137            }
138            _ => {
139                return Err(Fail(format!("field {address} should be an octal number")));
140            }
141        };
142
143        let non_contiuguous = match prev_plus_one {
144            None => true,
145            Some(expected) => expected != addr,
146        };
147
148        if non_contiuguous && let Some(old) = chunk.take() {
149            assert!(!old.is_empty());
150            binary.add_chunk(old);
151        }
152        let ch: &mut BinaryChunk = chunk.get_or_insert_with(|| BinaryChunk {
153            address: Address::from(addr),
154            words: Vec::new(),
155        });
156        prev_addr = Some(addr);
157        ch.push(word);
158    }
159    if let Some(current) = chunk.take()
160        && !current.is_empty()
161    {
162        binary.add_chunk(current);
163    }
164    Ok(binary)
165}
166
167fn run_utility() -> Result<(), Fail> {
168    let cli = Cli::parse();
169    let input_file = OpenOptions::new()
170        .read(true)
171        .open(cli.input)
172        .map_err(|e| Fail(format!("failed to open input file: {e}")))?;
173    let binary = read_binary(input_file)?;
174
175    let output_path = PathBuf::from(cli.output);
176    let output_file = OpenOptions::new()
177        .create(true)
178        .write(true)
179        .truncate(true)
180        .open(&output_path)
181        .map_err(|e| Fail(format!("failed to write output: {e}")))?;
182
183    let mut writer = BufWriter::new(output_file);
184    match write_user_program(&binary, &mut writer, &output_path) {
185        Ok(()) => Ok(()),
186        Err(e) => Err(Fail(e.to_string())),
187    }
188}
189
190fn main() {
191    match run_utility() {
192        Err(e) => {
193            eprintln!("{e}");
194            std::process::exit(1);
195        }
196        Ok(()) => {
197            std::process::exit(0);
198        }
199    }
200}