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