Skip to main content

ironwork_numeric/
options.rs

1use std::fmt;
2use zarch::ebcdic::CodePage;
3use zarch::hfp::Precision;
4
5/// Enterprise COBOL's compiler options, from Table 45 of IBM's Programming Guide, vendored byte for
6/// byte from cobolwork's provenance/enterprise-options.tsv (tools/sync-option-table.sh).
7const TABLE: &str = include_str!("../data/enterprise-options.tsv");
8
9/// An option in IBM's table: the spellings it answers to, and where it may be given.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct Documented {
12    pub name: &'static str,
13    spellings: &'static str,
14    /// Whether a CBL or PROCESS statement may give it.
15    pub process: bool,
16    /// Whether a CBL or PROCESS statement may give it only before a batch compilation's first program.
17    pub first_program_only: bool,
18    pub installation_default: bool,
19    /// The page of its section in the Enterprise COBOL 6.4 Programming Guide.
20    pub page: u16,
21}
22
23impl Documented {
24    pub fn spellings(&self) -> impl Iterator<Item = &'static str> {
25        self.spellings.split(' ')
26    }
27}
28
29/// Every option in IBM's table.
30pub fn documented() -> impl Iterator<Item = Documented> {
31    TABLE.lines().filter(|l| !l.starts_with('#')).map(|line| {
32        let f: Vec<&'static str> = line.split('\t').collect();
33        let yes = |i: usize| f[i] == "yes";
34        Documented { name: f[0], spellings: f[2], process: yes(3), first_program_only: yes(4), installation_default: yes(5), page: f[6].parse().expect("the table's pages are numbers") }
35    })
36}
37
38/// The option IBM documents under `spelling`, and whether the spelling turns the option off: a
39/// spelling beginning NO that is not the option's own name.
40pub fn spelled(spelling: &str) -> Option<(Documented, bool)> {
41    let o = documented().find(|o| o.spellings().any(|s| s == spelling))?;
42    Some((o, spelling.starts_with("NO") && !o.name.split('/').any(|n| n == spelling)))
43}
44
45/// Whether `option`, as a CBL or PROCESS statement writes it, sets the on-off option `name`, and to
46/// which: `SSR(ZLEN)` gives Some(true) for SSRANGE, `NOSSR` Some(false), and TRUNC(OPT) None.
47pub fn switch(option: &str, name: &str) -> Option<bool> {
48    let option = option.trim().to_ascii_uppercase();
49    let word = option.split('(').next().unwrap_or("").trim();
50    spelled(word).filter(|(o, _)| o.name == name).map(|(_, off)| !off)
51}
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub enum Arith {
55    #[default]
56    Compat,
57    Extend,
58}
59
60impl Arith {
61    pub const fn max_picture_digits(self) -> u32 {
62        match self {
63            Self::Compat => 18,
64            Self::Extend => 31,
65        }
66    }
67
68    pub const fn intermediate_digits(self) -> u32 {
69        match self {
70            Self::Compat => 30,
71            Self::Extend => 31,
72        }
73    }
74
75    pub const fn float_intermediate(self) -> Precision {
76        match self {
77            Self::Compat => Precision::Long,
78            Self::Extend => Precision::Extended,
79        }
80    }
81}
82
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub enum Trunc {
85    #[default]
86    Std,
87    Opt,
88    Bin,
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum Numproc {
93    #[default]
94    Nopfd,
95    Pfd,
96}
97
98/// Whether checked mode reports: a store that TRUNC(OPT) leaves to the generated code, when decimal
99/// and binary truncation disagree, and a SORT whose outcome FASTSRT changes. `-silent` turns the
100/// reports off.
101#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
102pub enum TruncCheck {
103    #[default]
104    Report,
105    Silent,
106}
107
108/// How SORT and MERGE compare a zoned or packed key: as DFSORT compares ZD and PD fields, so no
109/// bytes are invalid, or (`-strict-sort-keys`) as the program reads the item, so an invalid one is a
110/// data exception.
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
112pub enum SortKeys {
113    #[default]
114    Dfsort,
115    Strict,
116}
117
118/// Whether FASTSRT gives DFSORT the I/O of a USING or GIVING print file under ADV, whose data set's
119/// records are a byte longer than its FD's: never (`--fastsrt-adv-print=exclude`), or as any other
120/// file's (`--fastsrt-adv-print=include`).
121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
122pub enum FastsrtAdvPrint {
123    #[default]
124    Exclude,
125    Include,
126}
127
128impl FastsrtAdvPrint {
129    pub const fn flag(self) -> &'static str {
130        match self {
131            Self::Exclude => "--fastsrt-adv-print=exclude",
132            Self::Include => "--fastsrt-adv-print=include",
133        }
134    }
135}
136
137/// Whether a program whose compile gave warnings runs (`Proceed`), or (`-warnings-block`, the
138/// command line's NOCOMPILE(W)) is refused. The return code is 4 either way.
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
140pub enum Warnings {
141    #[default]
142    Proceed,
143    Block,
144}
145
146/// The COMPILE option: which messages stop the object code, so that run and cics refuse the
147/// program (Programming Guide SC27-8714-03, p. 355).
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum Compile {
150    /// COMPILE: object code whatever the messages, unless one is U and the compilation ended.
151    Full,
152    /// NOCOMPILE(W), NOCOMPILE(E) or NOCOMPILE(S): none from the first message of that severity up.
153    Until(Stop),
154    /// NOCOMPILE: a syntax check, with no object code at all.
155    SyntaxOnly,
156}
157
158/// A severity NOCOMPILE names.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub enum Stop {
161    W,
162    E,
163    S,
164}
165
166impl Default for Compile {
167    fn default() -> Self {
168        Self::Until(Stop::S)
169    }
170}
171
172impl Compile {
173    /// The lowest return code (Programming Guide Table 38, p. 282) of a message that stops the object
174    /// code: 0 stops it whatever the messages.
175    pub const fn stops_at(self) -> u8 {
176        match self {
177            Self::Full => 16,
178            Self::Until(Stop::S) => 12,
179            Self::Until(Stop::E) => 8,
180            Self::Until(Stop::W) => 4,
181            Self::SyntaxOnly => 0,
182        }
183    }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct Options {
188    pub arith: Arith,
189    pub trunc: Trunc,
190    pub numproc: Numproc,
191    pub codepage: u16,
192    pub trunc_check: TruncCheck,
193    /// FASTSRT: DFSORT does the I/O of a SORT's USING and GIVING files where IBM's rules allow.
194    pub fastsrt: bool,
195    pub fastsrt_adv_print: FastsrtAdvPrint,
196    pub sort_keys: SortKeys,
197    /// ADV: a print file's printer control character is a byte added before each record; under
198    /// NOADV it is the record's own first byte.
199    pub adv: bool,
200    /// THREAD, DLL, RENT and DBCS, which object-oriented programs are compiled with.
201    pub thread: bool,
202    pub dll: bool,
203    pub rent: bool,
204    pub dbcs: bool,
205    pub warnings: Warnings,
206    /// COMPILE or NOCOMPILE as a CBL or PROCESS card gave it; None when none did.
207    pub compile: Option<Compile>,
208    /// DYNAM: a CALL of a literal loads the program at run time, as a CALL of an identifier does.
209    pub dynam: bool,
210    /// The Language Environment runtime option DEBUG (`-debug`): USE FOR DEBUGGING procedures run.
211    /// NODEBUG, IBM's default, keeps them from running (assumption C63).
212    pub debug: bool,
213}
214
215impl Default for Options {
216    fn default() -> Self {
217        Self {
218            arith: Arith::default(),
219            trunc: Trunc::default(),
220            numproc: Numproc::default(),
221            codepage: 1140,
222            trunc_check: TruncCheck::default(),
223            fastsrt: false,
224            fastsrt_adv_print: FastsrtAdvPrint::default(),
225            sort_keys: SortKeys::default(),
226            adv: true,
227            thread: false,
228            dll: false,
229            rent: true,
230            dbcs: true,
231            warnings: Warnings::default(),
232            compile: None,
233            dynam: false,
234            debug: false,
235        }
236    }
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub enum OptionError {
241    BadSuboption { option: String, given: String },
242    Removed { option: String, since: &'static str },
243    UnsupportedCodePage(u16),
244    UnknownFlag(String),
245}
246
247impl fmt::Display for OptionError {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        match self {
250            Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
251            Self::Removed { option, since } => write!(f, "{option} was removed in Enterprise COBOL {since}"),
252            Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not a single-byte EBCDIC page this compiler carries"),
253            Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
254        }
255    }
256}
257
258impl std::error::Error for OptionError {}
259
260impl Options {
261    /// Applies one IBM compiler option as a CBL or PROCESS card or PARM writes it, e.g.
262    /// `TRUNC(OPT)`, `AR(E)`, `CP(1047)`. Returns false for an option this layer does not read.
263    pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
264        let option = option.trim().to_ascii_uppercase();
265        let (name, sub) = match option.split_once('(') {
266            Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
267            None => (option.as_str(), ""),
268        };
269        let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
270        let Some((documented, off)) = spelled(name) else { return Ok(false) };
271        match documented.name {
272            "ARITH" => {
273                self.arith = match sub {
274                    "COMPAT" | "C" => Arith::Compat,
275                    "EXTEND" | "E" => Arith::Extend,
276                    _ => return Err(bad()),
277                }
278            }
279            "TRUNC" => {
280                self.trunc = match sub {
281                    "STD" => Trunc::Std,
282                    "OPT" => Trunc::Opt,
283                    "BIN" => Trunc::Bin,
284                    _ => return Err(bad()),
285                }
286            }
287            "NUMPROC" => {
288                self.numproc = match sub {
289                    "NOPFD" => Numproc::Nopfd,
290                    "PFD" => Numproc::Pfd,
291                    "MIG" => return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5" }),
292                    _ => return Err(bad()),
293                }
294            }
295            "CODEPAGE" => {
296                let ccsid: u16 = sub.parse().map_err(|_| bad())?;
297                CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
298                self.codepage = ccsid;
299            }
300            "COMPILE" => {
301                self.compile = Some(match (off, sub) {
302                    (false, "") => Compile::Full,
303                    (true, "") => Compile::SyntaxOnly,
304                    (true, "W") => Compile::Until(Stop::W),
305                    (true, "E") => Compile::Until(Stop::E),
306                    (true, "S") => Compile::Until(Stop::S),
307                    _ => return Err(bad()),
308                })
309            }
310            "FASTSRT" => self.fastsrt = !off,
311            "ADV" => self.adv = !off,
312            "THREAD" => self.thread = !off,
313            "DLL" => self.dll = !off,
314            "RENT" => self.rent = !off,
315            "DBCS" => self.dbcs = !off,
316            "DYNAM" => self.dynam = !off,
317            _ => return Ok(false),
318        }
319        Ok(true)
320    }
321
322    /// Applies a flag of this compiler's own command line.
323    pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
324        match flag {
325            "-silent" => self.trunc_check = TruncCheck::Silent,
326            "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
327            "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
328            "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
329            "-warnings-block" => self.warnings = Warnings::Block,
330            "-debug" => self.debug = true,
331            _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
332        }
333        Ok(())
334    }
335
336    /// The COMPILE option in force: a card's, which outranks the command line as a PROCESS
337    /// statement outranks the compiler's invocation (Programming Guide SC27-8714-03, p. 273), else
338    /// NOCOMPILE(W) under `-warnings-block`, else IBM's default NOCOMPILE(S).
339    pub fn object_code(&self) -> Compile {
340        self.compile.unwrap_or(match self.warnings {
341            Warnings::Block => Compile::Until(Stop::W),
342            Warnings::Proceed => Compile::default(),
343        })
344    }
345
346    pub fn code_page(&self) -> &'static CodePage {
347        CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn defaults_are_ibms() {
357        let o = Options::default();
358        assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
359        assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
360    }
361
362    #[test]
363    fn dynam_and_its_abbreviations() {
364        let mut o = Options::default();
365        assert_eq!(o.apply("DYN"), Ok(true));
366        assert!(o.dynam);
367        assert_eq!(o.apply("NODYNAM"), Ok(true));
368        assert!(!o.dynam);
369    }
370
371    #[test]
372    fn adv_and_noadv_have_no_abbreviations() {
373        let mut o = Options::default();
374        assert_eq!(o.apply("noadv"), Ok(true));
375        assert!(!o.adv);
376        assert_eq!(o.apply("ADV"), Ok(true));
377        assert!(o.adv);
378        assert_eq!(o.apply("NOAD"), Ok(false));
379    }
380
381    #[test]
382    fn thread_dll_rent_and_dbcs_and_their_negatives() {
383        let mut o = Options::default();
384        for option in ["thread", "DLL", "NORENT", "NODBCS"] {
385            assert_eq!(o.apply(option), Ok(true));
386        }
387        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
388        for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
389            assert_eq!(o.apply(option), Ok(true));
390        }
391        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
392    }
393
394    #[test]
395    fn abbreviations_and_case() {
396        let mut o = Options::default();
397        assert_eq!(o.apply("ar(e)"), Ok(true));
398        assert_eq!(o.apply("CP(1047)"), Ok(true));
399        assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
400        assert_eq!(o.apply("fsrt"), Ok(true));
401        assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
402        assert_eq!(o.apply("NOFASTSRT"), Ok(true));
403        assert!(!o.fastsrt);
404    }
405
406    #[test]
407    fn ibms_table_is_whole_and_no_spelling_names_two_options() {
408        let all: Vec<Documented> = documented().collect();
409        assert_eq!(all.len(), 85, "Table 45 lists 85 options");
410        let mut seen = std::collections::HashMap::new();
411        for o in &all {
412            for s in o.spellings() {
413                assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
414            }
415        }
416        assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
417        let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
418        assert!(!adata.process && adata.page == 345);
419    }
420
421    #[test]
422    fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
423        let suboption = |name| match name {
424            "ARITH" => "(E)",
425            "CODEPAGE" => "(1047)",
426            "TRUNC" => "(OPT)",
427            "NUMPROC" => "(PFD)",
428            _ => "",
429        };
430        for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE"] {
431            let o = documented().find(|o| o.name == name).unwrap();
432            for s in o.spellings() {
433                assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
434            }
435        }
436        let mut o = Options::default();
437        o.apply("FSRT").unwrap();
438        o.apply("NOFSRT").unwrap();
439        assert!(!o.fastsrt);
440        assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
441        assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
442        assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
443        assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
444    }
445
446    /// Run with IRONWORK_COBOLWORK_DIR naming a cobolwork checkout to check the vendored copy.
447    #[test]
448    fn the_vendored_table_is_cobolworks() {
449        let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
450        let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
451        assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
452    }
453
454    #[test]
455    fn options_this_layer_does_not_read_pass_through() {
456        assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
457    }
458
459    #[test]
460    fn refusals_name_the_problem() {
461        let mut o = Options::default();
462        assert!(matches!(o.apply("NUMPROC(MIG)"), Err(OptionError::Removed { .. })));
463        assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
464        assert_eq!(o.apply("CODEPAGE(930)"), Err(OptionError::UnsupportedCodePage(930)));
465    }
466
467    #[test]
468    fn silent_flag_turns_off_trunc_reports() {
469        let mut o = Options::default();
470        o.apply_flag("-silent").unwrap();
471        assert_eq!(o.trunc_check, TruncCheck::Silent);
472        o.apply_flag("-strict-sort-keys").unwrap();
473        assert_eq!(o.sort_keys, SortKeys::Strict);
474        assert!(o.apply_flag("-quiet").is_err());
475    }
476
477    #[test]
478    fn warnings_proceed_unless_the_flag_blocks_them() {
479        let mut o = Options::default();
480        assert_eq!(o.warnings, Warnings::Proceed);
481        o.apply_flag("-warnings-block").unwrap();
482        assert_eq!(o.warnings, Warnings::Block);
483        assert!(o.apply_flag("-Werror").is_err());
484    }
485
486    #[test]
487    fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
488        let given = |option: &str| {
489            let mut o = Options::default();
490            o.apply(option).map(|_| o.compile)
491        };
492        assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
493        assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
494        assert_eq!(given("c"), Ok(Some(Compile::Full)));
495        assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
496        assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
497        assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
498        assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
499        for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
500            assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
501        }
502        let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
503        assert_eq!(codes, [16, 12, 8, 4, 0]);
504    }
505
506    #[test]
507    fn a_card_outranks_warnings_block() {
508        let mut o = Options::default();
509        o.apply_flag("-warnings-block").unwrap();
510        assert_eq!(o.object_code(), Compile::Until(Stop::W));
511        o.apply("NOCOMPILE(S)").unwrap();
512        assert_eq!(o.object_code(), Compile::Until(Stop::S));
513        o.apply("NOCOMPILE(E)").unwrap();
514        assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
515    }
516
517    #[test]
518    fn fastsrt_adv_print_excludes_unless_included() {
519        let mut o = Options::default();
520        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
521        o.apply_flag("--fastsrt-adv-print=include").unwrap();
522        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
523        o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
524        assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
525        assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
526        assert!(o.apply_flag("--fastsrt-adv-print").is_err());
527    }
528}