Skip to main content

ironwork_exec/
le.rs

1//! Language Environment callable services (SA38-0683-60): which ones ironwork provides, their
2//! condition tokens, Lilian dates and seconds, and the picture strings of the date and time
3//! services. The CALL side, which reads and writes the arguments, is machine/le_services.rs. What
4//! the manual leaves open is `numeric::assumptions` L1 to L18.
5
6use crate::calendar::{civil, days_in_month, is_leap, lilian, weekday, LAST_LILIAN, LILIAN_ZERO, MILLIS_PER_DAY, SECONDS_PER_DAY};
7use numeric::precision::{Fixed, Places};
8use zarch::check::ProgramMask;
9use zarch::ebcdic::{self, CodePage};
10use zarch::hfp::{Hfp, Precision, Rounding};
11use zarch::wide::U256;
12
13/// The services ironwork provides, each with its parameters in order; `fc` is always last.
14pub const PROVIDED: &[(&str, &[&str])] = &[
15    ("CEE3ABD", &["abcode", "clean-up"]),
16    ("CEE3DMP", &["title", "options", "fc"]),
17    ("CEEDATE", &["input_Lilian_date", "picture_string", "output_char_date", "fc"]),
18    ("CEEDATM", &["input_seconds", "picture_string", "output_timestamp", "fc"]),
19    ("CEEDAYS", &["input_char_date", "picture_string", "output_Lilian_date", "fc"]),
20    ("CEEDYWK", &["input_Lilian_date", "output_day_no", "fc"]),
21    ("CEEFRST", &["address", "fc"]),
22    ("CEEGMT", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
23    ("CEEGMTO", &["offset_hours", "offset_minutes", "offset_seconds", "fc"]),
24    ("CEEGTST", &["heap_id", "size", "address", "fc"]),
25    ("CEELOCT", &["output_Lilian", "output_seconds", "output_Gregorian", "fc"]),
26    ("CEEMOUT", &["message_string", "destination_code", "fc"]),
27    ("CEESECS", &["input_timestamp", "picture_string", "output_seconds", "fc"]),
28    ("CEEUTC", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
29];
30
31/// Every callable service SA38-0683-60 documents, other than the math services.
32const DOCUMENTED: &[&str] = &[
33    "CEE3AB2", "CEE3ABD", "CEE3CIB", "CEE3CTY", "CEE3DLY", "CEE3DMP", "CEE3GRC", "CEE3GRN", "CEE3GRO", "CEE3INF", "CEE3LNG", "CEE3MC2", "CEE3MCS",
34    "CEE3MDS", "CEE3MTS", "CEE3PR2", "CEE3PRM", "CEE3RPH", "CEE3SPM", "CEE3SRC", "CEE3SRP", "CEE3USR", "CEECBLDY", "CEECMI", "CEECRHP", "CEECZST",
35    "CEEDATE", "CEEDATM", "CEEDAYS", "CEEDCOD", "CEEDLYM", "CEEDSHP", "CEEDYWK", "CEEENV", "CEEFMDA", "CEEFMDT", "CEEFMON", "CEEFMTM", "CEEFRST",
36    "CEEFTDS", "CEEGMT", "CEEGMTO", "CEEGPID", "CEEGQDT", "CEEGTJS", "CEEGTST", "CEEHDLR", "CEEHDLU", "CEEISEC", "CEEITOK", "CEELCNV", "CEELOCT",
37    "CEEMGET", "CEEMICT", "CEEMOUT", "CEEMRCE", "CEEMRCR", "CEEMSG", "CEENCOD", "CEEQCEN", "CEEQDTC", "CEEQRYL", "CEERAN0", "CEERCDM", "CEESCEN",
38    "CEESCOL", "CEESECI", "CEESECS", "CEESETL", "CEESGL", "CEESICLR", "CEESISET", "CEESISHF", "CEESITST", "CEESTXF", "CEETDLI", "CEETEST", "CEEUSGD",
39    "CEEUTC",
40];
41
42/// The math services are CEESxnnn, x naming the operand type.
43const MATH: &[&str] = &[
44    "ABS", "ACS", "ASN", "ATH", "ATN", "AT2", "CJG", "COS", "CSH", "CTN", "DIM", "DVD", "ERC", "ERF", "EXP", "GMA", "IMG", "INT", "LGM", "LG1", "LG2",
45    "LOG", "MLT", "MOD", "NIN", "NWN", "SGN", "SIN", "SNH", "SQT", "TAN", "TNH",
46];
47
48pub fn parameters(name: &str) -> Option<&'static [&'static str]> {
49    PROVIDED.iter().find(|(n, _)| *n == name).map(|(_, p)| *p)
50}
51
52pub fn provides(name: &str) -> bool {
53    parameters(name).is_some()
54}
55
56fn documented(name: &str) -> bool {
57    let math = name.len() == 8 && name.starts_with("CEES") && (MATH.contains(&&name[5..]) || name[5..].starts_with("XP"));
58    DOCUMENTED.contains(&name) || math
59}
60
61/// The S806 message for a CALL that finds nothing.
62pub fn missing(name: &str) -> String {
63    if documented(name) {
64        format!("CALL {name}: {name} is a Language Environment callable service that ironwork for COBOL does not provide yet")
65    } else {
66        format!("CALL {name}: no such program in the run unit or its program libraries")
67    }
68}
69
70/// A condition of facility CEE: its severity and message number.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct Condition {
73    pub severity: u8,
74    pub number: u16,
75}
76
77const fn condition(severity: u8, number: u16) -> Condition {
78    Condition { severity, number }
79}
80
81pub const DESTINATION: Condition = condition(3, 451);
82pub const HEAP_ID: Condition = condition(3, 803);
83pub const HEAP_SIZE: Condition = condition(3, 808);
84pub const FREE_ADDRESS: Condition = condition(3, 810);
85pub const HEAP_SHORT: Condition = condition(3, 813);
86pub const SECONDS_RANGE: Condition = condition(3, 2505);
87pub const INSUFFICIENT: Condition = condition(3, 2507);
88pub const DATE_VALUE: Condition = condition(3, 2508);
89pub const HOURS: Condition = condition(3, 2510);
90pub const LILIAN_RANGE: Condition = condition(3, 2512);
91pub const DATE_RANGE: Condition = condition(3, 2513);
92pub const MINUTES: Condition = condition(3, 2516);
93pub const MONTH: Condition = condition(3, 2517);
94pub const PICTURE: Condition = condition(3, 2518);
95pub const SECONDS_VALUE: Condition = condition(3, 2519);
96pub const DAYS_NONNUMERIC: Condition = condition(3, 2520);
97pub const SECS_NONNUMERIC: Condition = condition(3, 2525);
98pub const DATE_TRUNCATED: Condition = condition(2, 2526);
99pub const TIMESTAMP_TRUNCATED: Condition = condition(2, 2527);
100pub const DUMP_OPTIONS: Condition = condition(2, 3102);
101
102impl Condition {
103    pub fn symbol(self) -> String {
104        syntax::feedback::symbol(self.number)
105    }
106
107    /// The 12-byte condition token, with no instance-specific information.
108    pub fn token(self) -> [u8; 12] {
109        let mut t = [0u8; 12];
110        t[..8].copy_from_slice(&syntax::feedback::token(self.severity, self.number));
111        t
112    }
113
114    pub fn text(self) -> &'static str {
115        match self.number {
116            451 => "CEEMOUT was given a destination code it does not accept.",
117            803 => "No heap has the identifier given to a get-storage or discard-heap request.",
118            808 => "A get-storage (CEEGTST) or reallocate (CEECZST) request asked for a size that is not positive.",
119            810 => "CEEFRST was given an address that is not heap storage, or the heap's control information is damaged.",
120            813 => "There was not enough storage to satisfy a get-storage request.",
121            2505 => "The number of seconds is outside the range the service supports.",
122            2507 => "CEEDAYS or CEESECS ran out of input before the date was complete, so no Lilian value was produced.",
123            2508 => "CEEDAYS or CEESECS was given a date that does not exist.",
124            2510 => "CEEISEC or CEESECS was given an hour it cannot accept.",
125            2512 => "CEEDATE or CEEDYWK was given a Lilian day outside the supported range.",
126            2513 => "CEEISEC, CEEDAYS or CEESECS was given a date outside the supported range.",
127            2516 => "CEEISEC was given a minute it cannot accept.",
128            2517 => "CEEISEC was given a month it cannot accept.",
129            2518 => "A date or time service was given a picture string it cannot use.",
130            2519 => "CEEISEC was given a second it cannot accept.",
131            2520 => "CEEDAYS found a non-digit where its picture wants a number, or the date does not fit the picture.",
132            2525 => "CEESECS found a non-digit where its picture wants a number, or the timestamp does not fit the picture.",
133            2526 => "The date CEEDATE produced did not fit its output field and was cut short.",
134            2527 => "The timestamp CEEDATM produced did not fit its output field and was cut short.",
135            3102 => "CEE3DMP ignored options or suboptions it does not recognise.",
136            _ => "",
137        }
138    }
139}
140
141/// An instant as a Lilian day and the milliseconds into it.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct Stamp {
144    pub lilian: i64,
145    pub millis: i64,
146}
147
148impl Stamp {
149    /// The clock's reading: Unix seconds and hundredths.
150    pub fn from_unix(seconds: i64, hundredths: u32) -> Self {
151        Self { lilian: seconds.div_euclid(SECONDS_PER_DAY) - LILIAN_ZERO, millis: seconds.rem_euclid(SECONDS_PER_DAY) * 1000 + i64::from(hundredths.min(99)) * 10 }
152    }
153
154    pub fn from_millis(ms: i64) -> Self {
155        Self { lilian: ms.div_euclid(MILLIS_PER_DAY), millis: ms.rem_euclid(MILLIS_PER_DAY) }
156    }
157
158    pub fn total_millis(self) -> i64 {
159        self.lilian.saturating_mul(MILLIS_PER_DAY).saturating_add(self.millis)
160    }
161
162    pub fn fields(self) -> Fields {
163        let c = civil(self.lilian.saturating_add(LILIAN_ZERO).saturating_mul(SECONDS_PER_DAY));
164        let s = self.millis / 1000;
165        Fields {
166            year: c.year,
167            month: c.month,
168            day: c.day,
169            yday: c.day_of_year,
170            weekday: weekday(self.lilian),
171            hour: (s / 3600) as u32,
172            minute: (s / 60 % 60) as u32,
173            second: (s % 60) as u32,
174            millis: (self.millis % 1000) as u32,
175        }
176    }
177
178    /// YYYYMMDDHHMISS999, as CEELOCT gives it.
179    pub fn gregorian(self) -> String {
180        let f = self.fields();
181        format!("{:04}{:02}{:02}{:02}{:02}{:02}{:03}", f.year, f.month, f.day, f.hour, f.minute, f.second, f.millis)
182    }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct Fields {
187    pub year: i64,
188    pub month: u32,
189    pub day: u32,
190    pub yday: u32,
191    pub weekday: u32,
192    pub hour: u32,
193    pub minute: u32,
194    pub second: u32,
195    pub millis: u32,
196}
197
198/// Lilian seconds as a COMP-2: the millisecond count, converted and divided by 1000 in long HFP.
199pub fn seconds_hfp(total_millis: i64) -> [u8; 8] {
200    let value = Fixed { negative: total_millis < 0, magnitude: U256::from_u128(u128::from(total_millis.unsigned_abs())), places: Places::new(16, 3) };
201    let bytes = numeric::float::from_fixed(value, Precision::Long, ProgramMask::default()).map_or(vec![0; 8], Hfp::to_bytes);
202    bytes.try_into().unwrap_or([0; 8])
203}
204
205/// A COMP-2 of Lilian seconds, to the nearest millisecond; None when it is not a number of
206/// milliseconds a fullword-pair can hold.
207pub fn hfp_millis(bytes: [u8; 8]) -> Option<i64> {
208    let (negative, ms) = Hfp::from_bytes(Precision::Long, &bytes).to_scaled_integer(3, Rounding::HalfAwayFromZero)?;
209    let ms = i64::try_from(ms.to_u128()?).ok()?;
210    Some(if negative { -ms } else { ms })
211}
212
213const MONTHS: [&str; 12] = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"];
214const DAYS: [&str; 7] = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"];
215const ROMAN: [&str; 12] = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
216
217/// One term of a date and time picture string (SA38-0683-60, Table 34).
218#[derive(Clone, Debug, PartialEq, Eq)]
219pub enum Term {
220    /// Y, YY or YYYY.
221    Year(u8),
222    /// MM, or ZM with its leading zero suppressed.
223    Month(bool),
224    /// MMM, Mmm and longer: each letter's case is the output's; Z suppresses trailing blanks.
225    MonthName { upper: Vec<bool>, trim: bool },
226    /// RRRR, or RRRZ.
227    Roman(bool),
228    Day(bool),
229    DayOfYear,
230    Hour(bool),
231    Minute,
232    Second,
233    /// 9, 99 or 999.
234    Fraction(u8),
235    Meridiem { lower: bool, dots: bool },
236    Weekday,
237    WeekdayName { upper: Vec<bool>, trim: bool },
238    /// <JJJJ>, <CCCC>, <CCCCCCCC>, YYY and ZYY, which ironwork does not provide.
239    Era,
240    /// A delimiter, kept as its byte.
241    Literal(u8),
242}
243
244impl Term {
245    fn is_time(&self) -> bool {
246        matches!(self, Term::Hour(_) | Term::Minute | Term::Second | Term::Fraction(_) | Term::Meridiem { .. })
247    }
248}
249
250/// The US defaults for a null or blank picture string (SA38-0683-60, Table 33).
251const DEFAULT_DATE: &str = "MM/DD/YY";
252const DEFAULT_TIMESTAMP: &str = "MM/DD/YY ZH:MI:SS AP";
253
254fn blank(picture: &[u8]) -> bool {
255    picture.iter().all(|&b| b == ebcdic::SPACE)
256}
257
258fn picture_or_default(picture: &[u8], default: &str, page: &CodePage) -> Vec<u8> {
259    if blank(picture) { page.encode(default).unwrap_or_default() } else { picture.to_vec() }
260}
261
262pub fn terms(picture: &[u8], page: &CodePage) -> Vec<Term> {
263    let chars: Vec<char> = picture.iter().map(|&b| page.decode_byte(b)).collect();
264    let at = |i: usize| chars.get(i).copied().unwrap_or('\0');
265    let run = |i: usize, f: fn(char) -> bool| chars[i..].iter().take_while(|&&c| f(c)).count();
266    let starts = |i: usize, s: &str| chars[i..].iter().copied().take(s.chars().count()).eq(s.chars());
267    // A Z or z after a name suppresses its blanks, unless it starts ZM, ZD, ZH or ZYY.
268    let trim = |i: usize| matches!(at(i), 'Z' | 'z') && !(at(i) == 'Z' && (matches!(at(i + 1), 'M' | 'D' | 'H') || starts(i + 1, "YY")));
269    let mut out: Vec<Term> = Vec::new();
270    let mut i = 0;
271    while i < chars.len() {
272        let after_hour = matches!(out.iter().rev().find(|t| !matches!(t, Term::Literal(_))), Some(Term::Hour(_)));
273        let (term, used) = match chars[i] {
274            'Y' => match run(i, |c| c == 'Y').min(4) {
275                3 => (Term::Era, 3),
276                n => (Term::Year(n as u8), n),
277            },
278            'Z' => match at(i + 1) {
279                'M' => (Term::Month(true), 2),
280                'D' => (Term::Day(true), 2),
281                'H' => (Term::Hour(true), 2),
282                'Y' if at(i + 2) == 'Y' => (Term::Era, 3),
283                _ => (Term::Literal(picture[i]), 1),
284            },
285            'M' if at(i + 1) == 'I' => (Term::Minute, 2),
286            'M' => match run(i, |c| c == 'M' || c == 'm') {
287                1 => (Term::Literal(picture[i]), 1),
288                2 if at(i + 1) == 'M' => (if after_hour { Term::Minute } else { Term::Month(false) }, 2),
289                2 => (Term::Literal(picture[i]), 1),
290                n => {
291                    let width = n.min(20);
292                    let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
293                    let trim = width == n && trim(i + width);
294                    (Term::MonthName { upper, trim }, width + usize::from(trim))
295                }
296            },
297            'W' => match run(i, |c| c == 'W' || c == 'w') {
298                1 | 2 => (Term::Weekday, 1),
299                n => {
300                    let width = n.min(20);
301                    let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
302                    let trim = width == n && trim(i + width);
303                    (Term::WeekdayName { upper, trim }, width + usize::from(trim))
304                }
305            },
306            'R' => match run(i, |c| c == 'R') {
307                n if n >= 4 => (Term::Roman(false), 4),
308                3 if at(i + 3) == 'Z' => (Term::Roman(true), 4),
309                _ => (Term::Literal(picture[i]), 1),
310            },
311            'D' => match run(i, |c| c == 'D') {
312                1 => (Term::Literal(picture[i]), 1),
313                2 => (Term::Day(false), 2),
314                _ => (Term::DayOfYear, 3),
315            },
316            'H' if at(i + 1) == 'H' => (Term::Hour(false), 2),
317            'S' if at(i + 1) == 'S' => (Term::Second, 2),
318            '9' => {
319                let n = run(i, |c| c == '9').min(3);
320                (Term::Fraction(n as u8), n)
321            }
322            'A' | 'a' if starts(i + 1, if chars[i] == 'A' { "P" } else { "p" }) => (Term::Meridiem { lower: chars[i] == 'a', dots: false }, 2),
323            'A' | 'a' if starts(i, if chars[i] == 'A' { "A.P." } else { "a.p." }) => (Term::Meridiem { lower: chars[i] == 'a', dots: true }, 4),
324            '<' if starts(i, "<JJJJ>") || starts(i, "<CCCC>") => (Term::Era, 6),
325            '<' if starts(i, "<CCCCCCCC>") => (Term::Era, 10),
326            _ => (Term::Literal(picture[i]), 1),
327        };
328        out.push(term);
329        i += used;
330    }
331    out
332}
333
334/// A name in the case its picture term asks for, padded or cut to the term's width.
335fn cased(name: &str, upper: &[bool], trim: bool) -> String {
336    let mut s: String = name.chars().zip(upper).map(|(c, &u)| if u { c } else { c.to_ascii_lowercase() }).collect();
337    if !trim {
338        while s.len() < upper.len() {
339            s.push(' ');
340        }
341    }
342    s
343}
344
345/// Writes a date, or a timestamp when `time` (CEEDATM); without it every time term is zero, as
346/// CEEDATE shows them (SA38-0683-60, Table 27).
347pub fn format(terms: &[Term], f: &Fields, time: bool, page: &CodePage) -> Vec<u8> {
348    let twelve = time && terms.iter().any(|t| matches!(t, Term::Meridiem { .. }));
349    let (hour, minute, second, millis) = if time { (f.hour, f.minute, f.second, f.millis) } else { (0, 0, 0, 0) };
350    let clock_hour = if twelve && hour % 12 == 0 { 12 } else if twelve { hour % 12 } else { hour };
351    let mut out = Vec::new();
352    for t in terms {
353        let text = match t {
354            Term::Literal(b) => {
355                out.push(*b);
356                continue;
357            }
358            Term::Year(4) => format!("{:04}", f.year),
359            Term::Year(2) => format!("{:02}", f.year % 100),
360            Term::Year(_) => format!("{}", f.year % 10),
361            Term::Month(true) => f.month.to_string(),
362            Term::Month(false) => format!("{:02}", f.month),
363            Term::MonthName { upper, trim } => cased(MONTHS[f.month as usize - 1], upper, *trim),
364            Term::Roman(trim) => cased(ROMAN[f.month as usize - 1], &[true; 4], *trim),
365            Term::Day(true) => f.day.to_string(),
366            Term::Day(false) => format!("{:02}", f.day),
367            Term::DayOfYear => format!("{:03}", f.yday),
368            Term::Hour(true) => clock_hour.to_string(),
369            Term::Hour(false) => format!("{clock_hour:02}"),
370            Term::Minute => format!("{minute:02}"),
371            Term::Second => format!("{second:02}"),
372            Term::Fraction(n) => format!("{:03}", millis)[..*n as usize].to_owned(),
373            Term::Meridiem { lower, dots } => {
374                let pm = time && hour >= 12;
375                let s = match (pm, dots) {
376                    (false, false) => "AM",
377                    (true, false) => "PM",
378                    (false, true) => "A.M.",
379                    (true, true) => "P.M.",
380                };
381                if *lower { s.to_ascii_lowercase() } else { s.to_owned() }
382            }
383            Term::Weekday => DAYS[f.weekday as usize - 1][..1].to_owned(),
384            Term::WeekdayName { upper, trim } => cased(DAYS[f.weekday as usize - 1], upper, *trim),
385            Term::Era => String::new(),
386        };
387        out.extend(text.chars().map(|c| page.encode_char(c).unwrap_or(ebcdic::SPACE)));
388    }
389    out
390}
391
392/// A fixed-length 80-character output, and whether the formatted text was longer.
393fn output_80(mut text: Vec<u8>) -> (Vec<u8>, bool) {
394    let truncated = text.len() > 80;
395    text.resize(80, ebcdic::SPACE);
396    (text, truncated)
397}
398
399/// CEEDATE: a Lilian date in the picture's form, 80 bytes, and the condition if any.
400pub fn date(lilian: i64, picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
401    let blanks = vec![ebcdic::SPACE; 80];
402    if !(1..=LAST_LILIAN).contains(&lilian) {
403        return (blanks, Some(LILIAN_RANGE));
404    }
405    let terms = terms(&picture_or_default(picture, DEFAULT_DATE, page), page);
406    if terms.contains(&Term::Era) {
407        return (blanks, Some(PICTURE));
408    }
409    let fields = Stamp { lilian, millis: 0 }.fields();
410    let (text, truncated) = output_80(format(&terms, &fields, false, page));
411    (text, truncated.then_some(DATE_TRUNCATED))
412}
413
414/// CEEDATM: Lilian seconds in the picture's form, 80 bytes, and the condition if any.
415pub fn timestamp(seconds: [u8; 8], picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
416    let blanks = vec![ebcdic::SPACE; 80];
417    let Some(ms) = hfp_millis(seconds).filter(|ms| (MILLIS_PER_DAY..(LAST_LILIAN + 1) * MILLIS_PER_DAY).contains(ms)) else {
418        return (blanks, Some(SECONDS_RANGE));
419    };
420    let terms = terms(&picture_or_default(picture, DEFAULT_TIMESTAMP, page), page);
421    if terms.contains(&Term::Era) {
422        return (blanks, Some(PICTURE));
423    }
424    let (text, truncated) = output_80(format(&terms, &Stamp::from_millis(ms).fields(), true, page));
425    (text, truncated.then_some(TIMESTAMP_TRUNCATED))
426}
427
428/// Which service is reading a date: CEEDAYS ignores time terms; CEESECS reads them.
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub enum Reading {
431    Days,
432    Seconds,
433}
434
435/// A numeric field of up to `width` digits, leading blanks allowed. A field before a delimiter may
436/// be shorter, as 6/2/88 is for MM/DD/YY; any other must be full. Width 0 is a delimiter, which
437/// takes one character, whatever it is.
438fn number(input: &[char], at: &mut usize, width: usize, delimited: bool, nonnumeric: Condition) -> Result<u32, Condition> {
439    if width == 0 {
440        *at += 1;
441        return Ok(0);
442    }
443    let (mut value, mut digits, mut taken) = (0u32, 0, 0);
444    while taken < width && *at + taken < input.len() {
445        match input[*at + taken] {
446            ' ' if digits == 0 => {}
447            c if c.is_ascii_digit() => {
448                value = value * 10 + c.to_digit(10).unwrap_or(0);
449                digits += 1;
450            }
451            _ => break,
452        }
453        taken += 1;
454    }
455    *at += taken;
456    let complete = digits > 0 && (taken == width || delimited);
457    match (complete, *at < input.len()) {
458        (true, _) => Ok(value),
459        (false, true) => Err(nonnumeric),
460        (false, false) => Err(INSUFFICIENT),
461    }
462}
463
464/// The run of characters `f` accepts, at most 20, uppercased.
465fn word(input: &[char], at: &mut usize, f: fn(&char) -> bool) -> String {
466    let w: String = input[*at..].iter().take_while(|c| f(c)).take(20).collect();
467    *at += w.chars().count();
468    w.to_ascii_uppercase()
469}
470
471#[derive(Default)]
472struct Parsed {
473    year: Option<(i64, bool)>,
474    month: Option<u32>,
475    day: Option<u32>,
476    yday: Option<u32>,
477    hour: Option<u32>,
478    minute: u32,
479    second: u32,
480    millis: u32,
481    pm: Option<bool>,
482}
483
484/// CEEDAYS and CEESECS: a date or timestamp read by its picture. `window` is the first year of
485/// the hundred a two-digit year falls in.
486pub fn read(input: &[u8], picture: &[u8], reading: Reading, window: i64, page: &CodePage) -> Result<Stamp, Condition> {
487    let default = if reading == Reading::Days { DEFAULT_DATE } else { DEFAULT_TIMESTAMP };
488    let picture = picture_or_default(picture, default, page);
489    let terms = terms(&picture, page);
490    if terms.iter().any(|t| matches!(t, Term::Era | Term::Year(1))) {
491        return Err(PICTURE);
492    }
493    let nonnumeric = if reading == Reading::Days { DAYS_NONNUMERIC } else { SECS_NONNUMERIC };
494    let input: Vec<char> = input.iter().map(|&b| page.decode_byte(b)).collect();
495    let lead = terms.iter().take_while(|t| **t == Term::Literal(ebcdic::SPACE)).count();
496    let mut at = if lead > 0 { lead } else { input.iter().take_while(|&&c| c == ' ').count() };
497    let mut p = Parsed::default();
498    for (k, t) in terms.iter().enumerate().skip(lead) {
499        if at >= input.len() {
500            if terms[k..].iter().any(|t| !t.is_time() && !matches!(t, Term::Literal(_) | Term::Weekday | Term::WeekdayName { .. })) {
501                return Err(INSUFFICIENT);
502            }
503            break;
504        }
505        let delimited = matches!(terms.get(k + 1), Some(Term::Literal(_)));
506        let mut number = |width: u8| number(&input, &mut at, width as usize, delimited, nonnumeric);
507        let outcome = match t {
508            Term::Literal(_) => number(0).map(drop),
509            Term::Year(n) => number(*n).map(|y| p.year = Some((i64::from(y), *n == 2))),
510            Term::Month(_) => number(2).map(|m| p.month = Some(m)),
511            Term::Day(_) => number(2).map(|d| p.day = Some(d)),
512            Term::DayOfYear => number(3).map(|d| p.yday = Some(d)),
513            Term::Hour(_) => number(2).map(|h| p.hour = Some(h)),
514            Term::Minute => number(2).map(|m| p.minute = m),
515            Term::Second => number(2).map(|s| p.second = s),
516            Term::Fraction(n) => number(*n).map(|f| p.millis = f * 10u32.pow(3 - u32::from(*n))),
517            Term::MonthName { .. } => {
518                let word = word(&input, &mut at, |c| c.is_ascii_alphabetic());
519                MONTHS.iter().position(|m| word.len() >= 3 && m.starts_with(&word)).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
520            }
521            Term::Roman(_) => {
522                let word = word(&input, &mut at, |c| matches!(c, 'I' | 'V' | 'X' | 'i' | 'v' | 'x'));
523                ROMAN.iter().position(|r| *r == word).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
524            }
525            Term::Weekday | Term::WeekdayName { .. } => {
526                word(&input, &mut at, |c| c.is_ascii_alphabetic());
527                Ok(())
528            }
529            Term::Meridiem { dots, .. } => {
530                let word: String = input[at..].iter().take(if *dots { 4 } else { 2 }).collect::<String>().to_ascii_uppercase();
531                at += word.chars().count();
532                match word.as_str() {
533                    "AM" | "A.M." => Some(false),
534                    "PM" | "P.M." => Some(true),
535                    _ => None,
536                }
537                .map(|pm| p.pm = Some(pm))
538                .ok_or(nonnumeric)
539            }
540            Term::Era => Err(PICTURE),
541        };
542        if !(reading == Reading::Days && t.is_time()) {
543            outcome?;
544        }
545    }
546    let Some((year, two_digit)) = p.year else { return Err(INSUFFICIENT) };
547    let year = if two_digit { window + (year - window).rem_euclid(100) } else { year };
548    let lilian = match (p.month, p.day, p.yday) {
549        (_, _, Some(yday)) if p.month.is_none() || p.day.is_none() => {
550            let length = if is_leap(year) { 366 } else { 365 };
551            if !(1..=length).contains(&yday) {
552                return Err(DATE_VALUE);
553            }
554            lilian(year, 1, 1) + i64::from(yday) - 1
555        }
556        (Some(month), Some(day), _) => {
557            if !(1..=12).contains(&month) {
558                return Err(MONTH);
559            }
560            if !(1..=days_in_month(year, month)).contains(&day) {
561                return Err(DATE_VALUE);
562            }
563            lilian(year, month, day)
564        }
565        _ => return Err(INSUFFICIENT),
566    };
567    if !(1582..=9999).contains(&year) || !(1..=LAST_LILIAN).contains(&lilian) {
568        return Err(DATE_RANGE);
569    }
570    if reading == Reading::Days {
571        return Ok(Stamp { lilian, millis: 0 });
572    }
573    let hour = match (p.hour.unwrap_or(0), p.pm) {
574        (h, None) if h <= 23 => h,
575        (h @ 1..=12, Some(pm)) => h % 12 + if pm { 12 } else { 0 },
576        _ => return Err(HOURS),
577    };
578    if p.minute > 59 {
579        return Err(MINUTES);
580    }
581    if p.second > 59 {
582        return Err(SECONDS_VALUE);
583    }
584    let millis = ((i64::from(hour) * 60 + i64::from(p.minute)) * 60 + i64::from(p.second)) * 1000 + i64::from(p.millis);
585    Ok(Stamp { lilian, millis })
586}
587
588/// What a run keeps for the services: heap storage, and which output DDs it has started.
589#[derive(Debug, Default)]
590pub struct State {
591    /// Each CEEGTST block: where it starts, its length, and whether CEEFRST has freed it.
592    pub heap: Vec<(usize, usize, bool)>,
593    pub written: Vec<String>,
594}
595
596impl State {
597    /// The end of the highest heap block: run-unit storage below it is not released.
598    pub fn heap_end(&self) -> usize {
599        self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
600    }
601}
602
603/// CEE3DMP's options: the ddname FNAME names, and whether any keyword was not one it takes.
604pub fn dump_options(options: &str) -> (String, bool) {
605    const KEYWORDS: &[(&str, usize)] = &[
606        ("ENCLAVE", 4), ("THREAD", 3), ("TRACEBACK", 5), ("NOTRACEBACK", 7), ("FILES", 4), ("NOFILES", 6), ("VARIABLES", 3),
607        ("NOVARIABLES", 5), ("BLOCKS", 5), ("NOBLOCKS", 7), ("STORAGE", 4), ("NOSTORAGE", 6), ("REGSTOR", 4), ("STACKFRAME", 2),
608        ("PAGESIZE", 4), ("FNAME", 5), ("CONDITION", 4), ("NOCONDITION", 6), ("ENTRY", 5), ("NOENTRY", 7), ("GENOPTS", 4), ("NOGENOPTS", 6),
609    ];
610    let mut dd = "CEEDUMP".to_owned();
611    let mut invalid = false;
612    let upper = options.to_ascii_uppercase();
613    let mut rest = upper.as_str();
614    loop {
615        rest = rest.trim_start_matches([' ', ',']);
616        if rest.is_empty() {
617            break;
618        }
619        let word_end = rest.find([' ', ',', '(']).unwrap_or(rest.len());
620        let (word, after) = rest.split_at(word_end);
621        let (argument, after) = match after.strip_prefix('(') {
622            Some(inner) => match inner.split_once(')') {
623                Some((a, tail)) => (Some(a.trim()), tail),
624                None => {
625                    invalid = true;
626                    (Some(inner.trim()), "")
627                }
628            },
629            None => (None, after),
630        };
631        match KEYWORDS.iter().find(|(k, min)| word.len() >= *min && k.starts_with(word)) {
632            Some(("FNAME", _)) => match argument.filter(|a| !a.is_empty()) {
633                Some(a) => dd = a.to_owned(),
634                None => invalid = true,
635            },
636            Some(_) => {}
637            None => invalid = true,
638        }
639        rest = after;
640    }
641    (dd, invalid)
642}
643
644#[cfg(test)]
645mod tests;