Skip to main content

ironwork_rt/
abend.rs

1//! The code an abend reports, as z/OS, CICS or ironwork names it, and the signals the interpreter
2//! passes up as errors to leave a statement early. With them, the I/O statuses FILE STATUS
3//! receives, and how a run ends.
4
5use crate::cics::Condition;
6use crate::vocab::Pos;
7use std::fmt;
8use zarch::check::ProgramCheck;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Abend {
12    pub code: AbendCode,
13    pub message: String,
14    pub pos: Pos,
15}
16
17impl Abend {
18    pub fn check(c: ProgramCheck, pos: Pos) -> Self {
19        Self { code: AbendCode::Check(c), message: format!("{c:?} exception"), pos }
20    }
21
22    pub fn ironwork(message: impl Into<String>, pos: Pos) -> Self {
23        Self { code: AbendCode::Ironwork, message: message.into(), pos }
24    }
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Ending {
29    Goback,
30    StopRun,
31    EndOfProgram,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum AbendCode {
36    /// A program check nothing handled: S0C6 to S0CF.
37    Check(ProgramCheck),
38    /// S0C4: an address outside the run unit's storage.
39    Protection,
40    /// S806: a CALLed program that is not in the library.
41    ModuleNotFound,
42    /// IO- and the status: a failing status with no FILE STATUS to hold it.
43    Io(FileStatus),
44    /// A CICS transaction abend code: a condition's default abend, ABCODE, ASRA.
45    Cics(String),
46    /// U and four decimal digits.
47    User(String),
48    Ironwork,
49    Exec,
50    Sql,
51    /// A call the SQL recording does not hold.
52    SqlReplay,
53    Java,
54    Signal(Signal),
55}
56
57/// Control flow passed up as an error to the statement that takes it.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum Signal {
60    /// STOP RUN in a USE BEFORE REPORTING procedure: the report statement that ran it ends the run.
61    StopRun,
62    /// GOBACK in a USE BEFORE REPORTING procedure.
63    GoBack,
64    /// RELEASE or RETURN stops the SORT or MERGE, with why as the message.
65    SortStopped,
66    /// The reader of DISPLAY output went away, as `head` does: not the program's failure.
67    ClosedOutput,
68    /// An EXCEPTION/ERROR procedure ended in GO TO, STOP RUN or GOBACK, which the statement that
69    /// ran it carries out.
70    DeclarativeExit,
71}
72
73impl Signal {
74    const ALL: [Self; 5] = [Self::StopRun, Self::GoBack, Self::SortStopped, Self::ClosedOutput, Self::DeclarativeExit];
75
76    fn text(self) -> &'static str {
77        match self {
78            Self::StopRun => "REPORT-STOP-RUN",
79            Self::GoBack => "REPORT-GOBACK",
80            Self::SortStopped => "SORT-STOPPED",
81            Self::ClosedOutput => "CLOSED-OUTPUT",
82            Self::DeclarativeExit => "DECLARATIVE-EXIT",
83        }
84    }
85}
86
87const CHECKS: [ProgramCheck; 10] = [
88    ProgramCheck::Specification,
89    ProgramCheck::Data,
90    ProgramCheck::FixedPointOverflow,
91    ProgramCheck::FixedPointDivide,
92    ProgramCheck::DecimalOverflow,
93    ProgramCheck::DecimalDivide,
94    ProgramCheck::HfpExponentOverflow,
95    ProgramCheck::HfpExponentUnderflow,
96    ProgramCheck::HfpSignificance,
97    ProgramCheck::HfpDivide,
98];
99
100fn check_code(c: ProgramCheck) -> &'static str {
101    match c {
102        ProgramCheck::Specification => "S0C6",
103        ProgramCheck::Data => "S0C7",
104        ProgramCheck::FixedPointOverflow => "S0C8",
105        ProgramCheck::FixedPointDivide => "S0C9",
106        ProgramCheck::DecimalOverflow => "S0CA",
107        ProgramCheck::DecimalDivide => "S0CB",
108        ProgramCheck::HfpExponentOverflow => "S0CC",
109        ProgramCheck::HfpExponentUnderflow => "S0CD",
110        ProgramCheck::HfpSignificance => "S0CE",
111        ProgramCheck::HfpDivide => "S0CF",
112    }
113}
114
115impl AbendCode {
116    pub fn user(code: u16) -> Self {
117        Self::User(format!("U{code:04}"))
118    }
119
120    pub fn as_str(&self) -> &str {
121        match self {
122            Self::Check(c) => check_code(*c),
123            Self::Protection => "S0C4",
124            Self::ModuleNotFound => "S806",
125            Self::Io(status) => status.abend_code(),
126            Self::Cics(code) | Self::User(code) => code,
127            Self::Ironwork => "IRONWORK",
128            Self::Exec => "EXEC",
129            Self::Sql => "SQL",
130            Self::SqlReplay => "SQLR",
131            Self::Java => "JAVA",
132            Self::Signal(s) => s.text(),
133        }
134    }
135
136    /// For SORT's FASTSRT reader, which still tests the text.
137    pub fn starts_with(&self, prefix: &str) -> bool {
138        self.as_str().starts_with(prefix)
139    }
140
141    /// The program check a zero divisor raises: decimal, fixed-point or HFP divide. An arithmetic
142    /// statement with ON SIZE ERROR takes it as a size error instead.
143    pub fn zero_divisor(&self) -> bool {
144        matches!(self, Self::Check(ProgramCheck::DecimalDivide | ProgramCheck::FixedPointDivide | ProgramCheck::HfpDivide))
145    }
146}
147
148impl fmt::Display for AbendCode {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.as_str())
151    }
152}
153
154impl PartialEq<&str> for AbendCode {
155    fn eq(&self, other: &&str) -> bool {
156        self.as_str() == *other
157    }
158}
159
160/// The code that prints as `text`, for the SQL runtime and SORT, which still name theirs as text.
161impl From<&str> for AbendCode {
162    fn from(text: &str) -> Self {
163        let user = text.len() == 5 && text.starts_with('U') && text[1..].bytes().all(|b| b.is_ascii_digit());
164        CHECKS
165            .map(Self::Check)
166            .into_iter()
167            .chain([Self::Protection, Self::ModuleNotFound, Self::Ironwork, Self::Exec, Self::Sql, Self::SqlReplay, Self::Java])
168            .chain(FileStatus::ALL.map(Self::Io))
169            .chain(Signal::ALL.map(Self::Signal))
170            .find(|c| c.as_str() == text)
171            .unwrap_or_else(|| if user { Self::User(text.into()) } else { Self::Cics(text.into()) })
172    }
173}
174
175/// An I/O status, as FILE STATUS receives it.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum FileStatus {
178    Success,
179    SuccessDuplicate,
180    SuccessWrongLength,
181    SuccessOptional,
182    AtEnd,
183    RelativeKeyOverflow,
184    SequenceError,
185    DuplicateKey,
186    NotFound,
187    BoundaryViolation,
188    PermanentError,
189    FileNotFound,
190    OpenModeUnsupported,
191    AlreadyOpen,
192    NotOpen,
193    NoPriorRead,
194    RecordLengthChanged,
195    NoNextRecord,
196    NotOpenInput,
197    NotOpenOutput,
198    NotOpenInputOutput,
199}
200
201impl FileStatus {
202    pub(crate) const ALL: [Self; 21] = [
203        Self::Success,
204        Self::SuccessDuplicate,
205        Self::SuccessWrongLength,
206        Self::SuccessOptional,
207        Self::AtEnd,
208        Self::RelativeKeyOverflow,
209        Self::SequenceError,
210        Self::DuplicateKey,
211        Self::NotFound,
212        Self::BoundaryViolation,
213        Self::PermanentError,
214        Self::FileNotFound,
215        Self::OpenModeUnsupported,
216        Self::AlreadyOpen,
217        Self::NotOpen,
218        Self::NoPriorRead,
219        Self::RecordLengthChanged,
220        Self::NoNextRecord,
221        Self::NotOpenInput,
222        Self::NotOpenOutput,
223        Self::NotOpenInputOutput,
224    ];
225
226    /// The code a run ends with when this status fails a statement and no FILE STATUS holds it.
227    pub fn abend_code(self) -> &'static str {
228        match self {
229            Self::Success => "IO-00",
230            Self::SuccessDuplicate => "IO-02",
231            Self::SuccessWrongLength => "IO-04",
232            Self::SuccessOptional => "IO-05",
233            Self::AtEnd => "IO-10",
234            Self::RelativeKeyOverflow => "IO-14",
235            Self::SequenceError => "IO-21",
236            Self::DuplicateKey => "IO-22",
237            Self::NotFound => "IO-23",
238            Self::BoundaryViolation => "IO-24",
239            Self::PermanentError => "IO-30",
240            Self::FileNotFound => "IO-35",
241            Self::OpenModeUnsupported => "IO-37",
242            Self::AlreadyOpen => "IO-41",
243            Self::NotOpen => "IO-42",
244            Self::NoPriorRead => "IO-43",
245            Self::RecordLengthChanged => "IO-44",
246            Self::NoNextRecord => "IO-46",
247            Self::NotOpenInput => "IO-47",
248            Self::NotOpenOutput => "IO-48",
249            Self::NotOpenInputOutput => "IO-49",
250        }
251    }
252
253    pub fn as_str(self) -> &'static str {
254        &self.abend_code()["IO-".len()..]
255    }
256
257    /// Whether the status is of class `class`, its first digit: 0 success, 1 AT END, 2 INVALID KEY.
258    pub fn covers(self, class: char) -> bool {
259        self.as_str().starts_with(class)
260    }
261
262    /// What a failing status means, for the message when no FILE STATUS or phrase takes it.
263    pub fn meaning(self) -> &'static str {
264        match self {
265            Self::AtEnd => "there is no next record",
266            Self::SequenceError => "the key is out of sequence",
267            Self::DuplicateKey => "a record with that key is already there",
268            Self::NotFound => "there is no record with that key",
269            Self::RelativeKeyOverflow => "the record number is too large for the RELATIVE KEY",
270            Self::BoundaryViolation => "the record number is outside the file",
271            Self::NoPriorRead => "the last statement on the file was not a successful READ",
272            Self::RecordLengthChanged => "the record is not the length of the one it replaces",
273            Self::NoNextRecord => "there is no next record: the last READ reached the end, or START found nothing",
274            Self::NotOpenInput => "the file is not open INPUT or I-O",
275            Self::NotOpenOutput => "the file is not open for output",
276            Self::NotOpenInputOutput => "the file is not open I-O",
277            _ => "the statement failed",
278        }
279    }
280
281    /// The CICS condition a keyed store's failure raises.
282    pub fn cics_condition(self) -> Condition {
283        match self {
284            Self::DuplicateKey => Condition::DUPREC,
285            Self::NotFound => Condition::NOTFND,
286            _ => Condition::INVREQ,
287        }
288    }
289}
290
291/// The status a two-digit code names, for SORT, which still passes its statuses as text.
292impl From<&str> for FileStatus {
293    fn from(code: &str) -> Self {
294        Self::ALL.into_iter().find(|s| s.as_str() == code).unwrap_or_else(|| panic!("{code} is not a file status ironwork sets"))
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn each_code_prints_as_the_string_it_replaced_and_parses_back_from_it() {
304        for c in CHECKS {
305            assert_eq!(AbendCode::Check(c).to_string(), c.abend());
306        }
307        for text in ["S0C7", "S0C4", "S806", "IO-35", "IO-46", "AEIP", "ASRA", "0999", "U4038", "IRONWORK", "EXEC", "SQL", "SQLR", "JAVA", "SORT-STOPPED", "CLOSED-OUTPUT"] {
308            assert_eq!(AbendCode::from(text).to_string(), text);
309        }
310        assert_eq!(AbendCode::from("U4038"), AbendCode::user(4038));
311        assert_eq!(AbendCode::from("IO-46"), AbendCode::Io(FileStatus::NoNextRecord));
312        assert_eq!(AbendCode::from("SORT-STOPPED"), AbendCode::Signal(Signal::SortStopped));
313    }
314}