cli/
main.rs

1/// Simulate the historic TX-2 computer
2mod clock;
3mod lw;
4mod sleep;
5
6use std::ffi::{OsStr, OsString};
7use std::fmt::{self, Debug, Display, Formatter};
8use std::fs::OpenOptions;
9use std::io::{BufReader, Read};
10use std::str::FromStr;
11use std::time::Duration;
12
13use clap::{ArgAction::Set, Parser, ValueEnum};
14use tracing::{Level, event};
15use tracing_subscriber::filter::{EnvFilter, LevelFilter};
16use tracing_subscriber::prelude::*;
17
18use base::prelude::*;
19use clock::{BasicClock, Clock};
20use cpu::{
21    self, Alarm, AlarmDetails, MemoryConfiguration, OutputEvent, ResetMode, RunMode, Tx2,
22    UnmaskedAlarm,
23};
24
25// Thanks to Google for allowing this code to be open-sourced.  I
26// generally prefer to correspond about this project using my
27// personal email address rather than my work one, though.
28const AUTHOR: &str = "James Youngman <james@youngman.org>";
29
30fn run(
31    tx2: &mut Tx2,
32    clk: &mut BasicClock,
33    sleep_multiplier: Option<f64>,
34) -> Result<(), Box<dyn std::error::Error>> {
35    if let Err(e) = tx2.codabo(&clk.make_fresh_context(), &ResetMode::ResetTSP) {
36        event!(Level::ERROR, "CODABO failed: {}", e);
37        return Err(Box::new(e));
38    }
39
40    // TODO: setting next_execution_due is basically the 'start'
41    // operaiton of the TX-2's sync system.  We should model that
42    // directly as the user may want to use the UI to operate the sync
43    // system as was possible in the real hardware.
44    {
45        let now = clk.now();
46        tx2.set_next_execution_due(now, Some(now));
47    }
48    tx2.set_run_mode(RunMode::Running);
49
50    match run_until_alarm(tx2, clk, sleep_multiplier) {
51        UnmaskedAlarm {
52            alarm,
53            address: Some(addr),
54            when: _,
55        } => {
56            event!(
57                Level::ERROR,
58                "Execution stopped at address  {:o}: {}",
59                addr,
60                alarm
61            );
62        }
63        UnmaskedAlarm {
64            alarm,
65            address: None,
66            when: _,
67        } => {
68            event!(Level::ERROR, "Execution stopped: {}", alarm);
69        }
70    };
71    if let Err(e) = tx2.disconnect_all_devices(&clk.make_fresh_context()) {
72        event!(Level::ERROR, "Failed in device shutdown: {}", e);
73        return Err(Box::new(e));
74    }
75    Ok(())
76}
77
78fn run_until_alarm(
79    tx2: &mut Tx2,
80    clk: &mut BasicClock,
81    sleep_multiplier: Option<f64>,
82) -> UnmaskedAlarm {
83    let mut sleeper = sleep::MinimalSleeper::new(Duration::from_millis(2));
84    let mut lw66 = lw::LincolnStreamWriter::new();
85
86    let result: UnmaskedAlarm = loop {
87        {
88            let now = clk.now();
89            let next = tx2.next_tick();
90            if now < next {
91                let interval = next - now;
92                sleep::time_passes(clk, &mut sleeper, &interval, sleep_multiplier);
93            }
94            clk.advance_to_simulated_time(next);
95        }
96        let tick_context = clk.make_fresh_context();
97        match tx2.tick(&tick_context) {
98            Ok(maybe_output) => {
99                match maybe_output {
100                    None => (),
101                    Some(OutputEvent::LincolnWriterPrint { unit, ch }) => {
102                        if unit == u6!(0o66) {
103                            if let Err(e) = lw66.write(ch) {
104                                event!(Level::ERROR, "output error: {}", e);
105                                // TODO: change state of the output unit to
106                                // indicate that there has been a failure,
107                                // instead of just terminating the simulation.
108                                break UnmaskedAlarm {
109                                    alarm: Alarm {
110                                        sequence: None,
111                                        details: AlarmDetails::MISAL {
112                                            affected_unit: u6!(0o66),
113                                        },
114                                    },
115                                    address: None,
116                                    when: tick_context.simulated_time,
117                                };
118                            }
119                        } else {
120                            event!(
121                                Level::WARN,
122                                "discarding Lincoln Writer output for unit {:o}",
123                                unit,
124                            );
125                        }
126                    }
127                }
128            }
129            Err(unmasked_alarm) => {
130                break unmasked_alarm;
131            }
132        }
133        let next_tick = tx2.next_tick();
134        if next_tick <= tick_context.simulated_time {
135            event!(
136                Level::WARN,
137                "Tx2::tick is not advancing the system clock (next tick {:?} <= current tick {:?})",
138                next_tick,
139                tick_context.simulated_time,
140            );
141        }
142    };
143    lw66.disconnect();
144    result
145}
146
147/// Whether to panic when there was an unmasked alarm.
148#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
149enum PanicOnUnmaskedAlarm {
150    // Panic when an unmasked alarm occurs.  If the RUST_BACKTRACE
151    // environment variable is also set, a stack backtrace will be
152    // printed.
153    No,
154    /// Do not panic when an unmasked alarm occurs.  The emulator will
155    /// stop, but no panic will happen.
156    Yes,
157}
158
159impl FromStr for PanicOnUnmaskedAlarm {
160    type Err = String;
161
162    fn from_str(s: &str) -> Result<Self, String> {
163        match s {
164            "true" | "yes" => Ok(PanicOnUnmaskedAlarm::Yes),
165            "false" | "no" => Ok(PanicOnUnmaskedAlarm::No),
166            _ => Err(format!(
167                "unexpected value '{s}': expected 'true', 'false', 'yes' or 'no'",
168            )),
169        }
170    }
171}
172
173/// Command-line simulator for the historical TX-2 computer
174#[derive(Parser, Debug)]
175#[command(author = AUTHOR, version, about, long_about = None)]
176struct Cli {
177    /// Run this many times faster than real-time ('MAX' for as-fast-as-possible)
178    #[arg(action = Set, long = "speed-multiplier")]
179    speed_multiplier: Option<String>,
180
181    /// When set, panic if an alarm occurs (so that a stack backtrace
182    /// is produced when the RUST_BACKTRACE environment variable is
183    /// also set).  When unset, stop the emulator without panic.
184    #[arg(action = Set, long = "panic-on-unmasked-alarm", value_enum)]
185    panic_on_unmasked_alarm: Option<PanicOnUnmaskedAlarm>,
186
187    /// File containing paper tape data
188    #[arg(action = Set)]
189    tape: Option<OsString>,
190}
191
192#[derive(Debug)]
193struct BadSpeedMultiplier(f64);
194
195impl Display for BadSpeedMultiplier {
196    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
197        write!(
198            f,
199            "speed multiplier {} is too small for reliable arithmetic operations",
200            self.0
201        )
202    }
203}
204
205impl std::error::Error for BadSpeedMultiplier {}
206
207fn run_simulator() -> Result<(), Box<dyn std::error::Error>> {
208    let cli = Cli::parse();
209
210    // By default, display info messages.
211    let env_filter = EnvFilter::builder()
212        .with_default_directive(LevelFilter::INFO.into())
213        .from_env_lossy();
214
215    // See https://docs.rs/tracing-subscriber/0.3.11/tracing_subscriber/filter/struct.EnvFilter.html#examples
216    tracing_subscriber::registry()
217        .with(tracing_subscriber::fmt::layer())
218        .with(env_filter)
219        .init();
220
221    let mem_config = MemoryConfiguration {
222        with_u_memory: false,
223    };
224
225    fn file_size_guess(file_name: &OsStr) -> Option<usize> {
226        match std::fs::metadata(file_name) {
227            Ok(metadata) => usize::try_from(metadata.len()).ok(),
228            Err(_) => None,
229        }
230    }
231
232    let tape_data: Option<Vec<u8>> = match cli.tape.as_ref() {
233        None => {
234            event!(
235                Level::WARN,
236                "No paper tapes were specified on the command line, so no program will be loaded"
237            );
238            None
239        }
240        Some(file_name) => {
241            match OpenOptions::new()
242                .read(true)
243                .open(file_name)
244                .map(BufReader::new)
245            {
246                Ok(mut r) => {
247                    let mut buf: Vec<u8> =
248                        Vec::with_capacity(file_size_guess(file_name).unwrap_or(0));
249                    if let Err(e) = r.read_to_end(&mut buf) {
250                        return Err(Box::new(e));
251                    } else {
252                        Some(buf)
253                    }
254                }
255                Err(e) => {
256                    return Err(Box::new(e));
257                }
258            }
259        }
260    };
261
262    let speed_multiplier: Option<f64> = match cli.speed_multiplier.as_ref() {
263        None => {
264            event!(
265                Level::WARN,
266                "No --speed-multiplier option specified, running at maximum speed"
267            );
268            None
269        }
270        Some(value) => match value.as_str() {
271            "MAX" => {
272                event!(
273                    Level::INFO,
274                    "--speed-multiplier=MAX, running at maximum speed"
275                );
276                None
277            }
278            some_number => match some_number.parse::<f64>() {
279                Ok(x) => {
280                    event!(
281                        Level::INFO,
282                        "--speed-multiplier={}, running at speed multiplier {}",
283                        some_number,
284                        x
285                    );
286                    Some(x)
287                }
288                Err(e) => {
289                    return Err(Box::new(e));
290                }
291            },
292        },
293    };
294    let sleep_multiplier = match speed_multiplier {
295        None => None,
296        Some(sp) => {
297            let sleep_multiplier = sp.recip();
298            if sleep_multiplier.is_finite() {
299                Some(sleep_multiplier)
300            } else {
301                return Err(Box::new(BadSpeedMultiplier(sp)));
302            }
303        }
304    };
305
306    let mut clk: BasicClock = BasicClock::new();
307    // We have two simimarly-named enums here so that the cpu crate
308    // does not have to depend on clap.
309    let panic_on_unmasked_alarm = match cli.panic_on_unmasked_alarm {
310        Some(PanicOnUnmaskedAlarm::Yes) => cpu::PanicOnUnmaskedAlarm::Yes,
311        Some(PanicOnUnmaskedAlarm::No) | None => cpu::PanicOnUnmaskedAlarm::No,
312    };
313    let initial_context = clk.make_fresh_context();
314    let mut tx2 = Tx2::new(&initial_context, panic_on_unmasked_alarm, &mem_config);
315    if let Some(tape) = tape_data
316        && let Err(e) = tx2.mount_paper_tape(&initial_context, tape)
317    {
318        return Err(Box::new(e));
319    }
320    run(&mut tx2, &mut clk, sleep_multiplier)
321}
322
323fn main() {
324    match run_simulator() {
325        Err(e) => {
326            eprintln!("{e}");
327            std::process::exit(1);
328        }
329        Ok(()) => {
330            std::process::exit(0);
331        }
332    }
333}