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