cpu/
alarmunit.rs

1//! The TX-2 can "mask" some alarms, and whether or not this is
2//! happening is controlled by the `AlarmUnit`.
3use std::collections::{BTreeMap, BTreeSet};
4
5use serde::Serialize;
6use tracing::{Level, event};
7
8use super::alarm::{Alarm, AlarmDetails, AlarmKind, AlarmMaskability, Alarmer, BugActivity};
9use super::changelog::ChangeIndex;
10use super::diagnostics::DiagnosticFetcher;
11use crate::diagnostics::CurrentInstructionDiagnostics;
12
13#[cfg(test)]
14use base::Unsigned6Bit;
15
16/// The status of an alarm.
17#[derive(Debug, Serialize)]
18pub struct AlarmStatus {
19    pub name: String,
20    pub maskable: bool,
21    pub masked: bool,
22    pub active: bool,
23    /// An explanation for the current situation; this was not a
24    /// faature of the original TX-2.
25    pub message: String,
26}
27
28/// An alarm is in one of the following states:
29///
30/// - inactive: it's not happening
31/// - firing: it's happening and not masked (execution will stop)
32/// - active but not firing (visible on the console, execution continues)
33#[derive(Debug, Default)]
34pub struct AlarmUnit {
35    panic_on_unmasked_alarm: bool,
36    masked: BTreeSet<AlarmKind>,
37    active: BTreeMap<AlarmKind, Alarm>,
38    changes: ChangeIndex<AlarmKind>,
39}
40
41impl AlarmUnit {
42    pub fn new() -> AlarmUnit {
43        AlarmUnit::default()
44    }
45
46    fn status_for_alarm_kind(&self, kind: &AlarmKind) -> AlarmStatus {
47        let maybe_firing_alarm: Option<&Alarm> = self.active.get(kind);
48        AlarmStatus {
49            name: kind.to_string(),
50            maskable: matches!(kind.maskable(), AlarmMaskability::Maskable),
51            masked: self.masked.contains(kind),
52            active: maybe_firing_alarm.is_some(),
53            message: match maybe_firing_alarm {
54                Some(a) => a.to_string(),
55                None => String::new(),
56            },
57        }
58    }
59
60    pub fn get_alarm_statuses(&self) -> Vec<AlarmStatus> {
61        AlarmKind::all_alarm_kinds()
62            .iter()
63            .map(|kind| self.status_for_alarm_kind(kind))
64            .collect()
65    }
66
67    pub fn drain_alarm_changes(&mut self) -> BTreeMap<AlarmKind, AlarmStatus> {
68        self.changes
69            .drain()
70            .into_iter()
71            .map(|kind| (kind, self.status_for_alarm_kind(&kind)))
72            .collect()
73    }
74
75    pub fn get_status_of_alarm(&self, name: &str) -> Option<AlarmStatus> {
76        AlarmKind::try_from(name)
77            .map(|k| self.status_for_alarm_kind(&k))
78            .ok()
79    }
80
81    pub fn new_with_panic(panic: bool) -> AlarmUnit {
82        AlarmUnit {
83            panic_on_unmasked_alarm: panic,
84            ..AlarmUnit::new()
85        }
86    }
87
88    pub fn mask(
89        &mut self,
90        kind: AlarmKind,
91        diags: &CurrentInstructionDiagnostics,
92    ) -> Result<(), Alarm> {
93        match kind.maskable() {
94            AlarmMaskability::Unmaskable => {
95                let bug = Alarm {
96                    sequence: None,
97                    details: AlarmDetails::BUGAL {
98                        activity: BugActivity::AlarmHandling,
99                        diagnostics: diags.clone(),
100                        message: format!("attempt to mask unmaskable alarm {kind}"),
101                    },
102                };
103                Err(self.always_fire(bug, diags))
104            }
105            AlarmMaskability::Maskable => {
106                self.changes.add(kind);
107                self.masked.insert(kind);
108                Ok(())
109            }
110        }
111    }
112
113    pub fn unmask(&mut self, kind: AlarmKind) {
114        if self.masked.remove(&kind) {
115            self.changes.add(kind);
116        }
117    }
118
119    fn is_masked(&self, alarm_instance: &Alarm) -> bool {
120        let kind = alarm_instance.kind();
121        match kind.maskable() {
122            AlarmMaskability::Unmaskable => false,
123            AlarmMaskability::Maskable => {
124                // TODO: is this correct for writes to un-mapped
125                // memory?
126                self.masked.contains(&kind)
127            }
128        }
129    }
130
131    fn maybe_panic(&self, alarm_instance: &Alarm) {
132        if self.panic_on_unmasked_alarm {
133            // We log an error event here primarily because the
134            // current tracing span includes the program counter
135            // value.
136            event!(Level::ERROR, "panicing with alarm {}", alarm_instance);
137            panic!(
138                "unmasked alarm and panic_on_unmasked_alarm={}: {}",
139                self.panic_on_unmasked_alarm, alarm_instance
140            );
141        }
142    }
143
144    pub fn clear_all_alarms(&mut self) {
145        event!(Level::INFO, "clearing all alarms");
146        self.active.clear();
147    }
148
149    pub fn unmasked_alarm_active(&self) -> bool {
150        self.active.keys().any(|kind| match kind.maskable() {
151            AlarmMaskability::Unmaskable => {
152                assert!(!self.masked.contains(kind));
153                true
154            }
155            AlarmMaskability::Maskable => !self.masked.contains(kind),
156        })
157    }
158
159    fn set_active(&mut self, alarm_instance: Alarm) -> Result<(), Alarm> {
160        let kind: AlarmKind = alarm_instance.kind();
161        self.changes.add(kind);
162        if self.is_masked(&alarm_instance) {
163            self.active.insert(kind, alarm_instance);
164            Ok(())
165        } else {
166            self.active.insert(kind, alarm_instance.clone());
167            self.maybe_panic(&alarm_instance);
168            Err(alarm_instance)
169        }
170    }
171}
172
173impl Alarmer for AlarmUnit {
174    fn fire_if_not_masked<F: DiagnosticFetcher>(
175        &mut self,
176        alarm_instance: Alarm,
177        _get_diags: F,
178    ) -> Result<(), Alarm> {
179        self.changes.add(alarm_instance.kind());
180        self.set_active(alarm_instance)
181    }
182
183    fn always_fire<F: DiagnosticFetcher>(
184        &mut self,
185        alarm_instance: Alarm,
186        get_diagnostics: F,
187    ) -> Alarm {
188        let kind = alarm_instance.kind();
189        self.changes.add(kind);
190        let sequence = alarm_instance.sequence;
191        match self.set_active(alarm_instance) {
192            Err(a) => a,
193            Ok(()) => {
194                let bug = Alarm {
195                    sequence,
196                    details: AlarmDetails::BUGAL {
197                        activity: BugActivity::AlarmHandling,
198                        diagnostics: get_diagnostics.diagnostics(),
199                        message: format!(
200                            "alarm {kind} is masked, but the caller assumed it could not be"
201                        ),
202                    },
203                };
204                match self.set_active(bug) {
205                    Err(a) => a,
206                    Ok(()) => unreachable!("Alarm BUGAL was unexpectedly masked"),
207                }
208            }
209        }
210    }
211}
212
213#[test]
214fn unmaskable_alarms_are_not_maskable() {
215    use base::prelude::{Address, Instruction};
216
217    let mut alarm_unit = AlarmUnit::new_with_panic(false);
218    assert!(!alarm_unit.unmasked_alarm_active());
219    let diagnostics = CurrentInstructionDiagnostics {
220        current_instruction: Instruction::invalid(),
221        instruction_address: Address::ZERO,
222    };
223    // Any attempt to mask an unmaskable alarm should itself result in an error.
224    assert!(
225        alarm_unit
226            .mask(AlarmKind::ROUNDTUITAL, &diagnostics)
227            .is_err()
228    );
229    // Now we raise some non-maskable alarm.
230    assert!(matches!(
231        alarm_unit.fire_if_not_masked(
232            Alarm {
233                sequence: Some(Unsigned6Bit::ZERO),
234                details: AlarmDetails::ROUNDTUITAL {
235                    explanation: "The ROUNDTUITAL alarm is not maskable!".to_string(),
236                    bug_report_url: "https://github.com/TX-2/TX-2-simulator/issues/144",
237                },
238            },
239            &diagnostics
240        ),
241        Err(Alarm {
242            sequence: Some(_),
243            details: AlarmDetails::ROUNDTUITAL { .. },
244        })
245    ));
246    // Verify that the alarm manager considers that an unmasked (in
247    // this case because unmaskable) alarm is active.
248    assert!(alarm_unit.unmasked_alarm_active());
249}
250
251#[test]
252fn maskable_alarms_are_not_masked_by_default() {
253    use base::prelude::{Address, Instruction};
254
255    let mut alarm_unit = AlarmUnit::new_with_panic(false);
256    assert!(!alarm_unit.unmasked_alarm_active());
257    let diagnostics = CurrentInstructionDiagnostics {
258        current_instruction: Instruction::invalid(),
259        instruction_address: Address::ZERO,
260    };
261    // Now we raise some maskable, but not masked, alarm.
262    let the_alarm = Alarm {
263        sequence: Some(Unsigned6Bit::ZERO),
264        details: AlarmDetails::PSAL(22, "some PPSAL alarm".to_string()),
265    };
266    // raise the alarm, verify that it really fires.
267    assert!(matches!(
268        alarm_unit.fire_if_not_masked(the_alarm, &diagnostics),
269        Err(Alarm {
270            sequence: _,
271            details: AlarmDetails::PSAL(22, _),
272        },)
273    ));
274    // Verify that the alarm manager considers that an unmasked (in
275    // this case because maskable but not actually masked) alarm is active.
276    assert!(alarm_unit.unmasked_alarm_active());
277}