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