Skip to main content

ironwork_numeric/
options.rs

1use std::fmt;
2use zarch::ebcdic::{self, 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/// INVDATA: zoned and packed items may hold invalid digits, sign codes or zone bits. FORCENUMCMP
138/// compares zoned items as numbers whatever their zones; CLEANSIGN cleans a sign code on input to
139/// a comparison or computation (Programming Guide SC27-8714-03, pp. 376-378). ZONEDATA(NOPFD) and
140/// ZONEDATA(MIG) are INVDATA with FORCENUMCMP off and on.
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub struct Invdata {
143    pub forcenumcmp: bool,
144    pub cleansign: bool,
145}
146
147impl Default for Invdata {
148    fn default() -> Self {
149        Self { forcenumcmp: false, cleansign: true }
150    }
151}
152
153/// What a program with no STOP RUN, GOBACK or EXIT PROGRAM that ends with EXEC CICS RETURN or XCTL
154/// gets (assumption C124): IBM's warning (`--cics-return-warning=always`), one informational note
155/// in place of it (`once`), or nothing (`never`).
156#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
157pub enum CicsReturnWarning {
158    #[default]
159    Once,
160    Always,
161    Never,
162}
163
164impl CicsReturnWarning {
165    pub const fn flag(self) -> &'static str {
166        match self {
167            Self::Once => "--cics-return-warning=once",
168            Self::Always => "--cics-return-warning=always",
169            Self::Never => "--cics-return-warning=never",
170        }
171    }
172}
173
174/// What the figurative constant QUOTE is: a quotation mark under QUOTE, IBM's default, or an
175/// apostrophe under APOST (Programming Guide SC27-8714-03, p. 347).
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
177pub enum Quote {
178    #[default]
179    Quote,
180    Apost,
181}
182
183impl Quote {
184    /// The character in EBCDIC, where every single-byte page carries it at the same place.
185    pub const fn byte(self) -> u8 {
186        match self {
187            Self::Quote => ebcdic::QUOTE,
188            Self::Apost => ebcdic::APOSTROPHE,
189        }
190    }
191
192    /// The character as a UTF-16 unit, for a national item.
193    pub const fn unit(self) -> u16 {
194        match self {
195            Self::Quote => 0x0022,
196            Self::Apost => 0x0027,
197        }
198    }
199}
200
201/// CURRENCY(literal): the character, or a hexadecimal literal's byte, which the program's code page
202/// turns into one (Programming Guide SC27-8714-03, p. 358).
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub enum Currency {
205    Char(char),
206    Hex(u8),
207}
208
209/// Whether N literals, and PICTURE N items with no USAGE clause, are national, IBM's default, or
210/// DBCS (Programming Guide SC27-8714-03, pp. 387-388).
211#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
212pub enum Nsymbol {
213    #[default]
214    National,
215    Dbcs,
216}
217
218/// How DISPLAY shows a signed binary, packed or overpunched zoned item: as releases before 6 did,
219/// with an overpunched digit (`Compat`), or with a separate leading sign (`Sep`) (Programming
220/// Guide SC27-8714-03, pp. 362-363).
221#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
222pub enum DispSign {
223    #[default]
224    Compat,
225    Sep,
226}
227
228/// Day 1 of the date intrinsic functions' integer dates: 1 January 1601 (`Ansi`), or Language
229/// Environment's Lilian 15 October 1582 (Programming Guide SC27-8714-03, p. 375).
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub enum IntDate {
232    #[default]
233    Ansi,
234    Lilian,
235}
236
237/// Whether a reference must be unique by the standard's rules (`Compat`), or resolves to the one
238/// item a complete set of qualifiers names (`Extend`) (Programming Guide SC27-8714-03, p. 400).
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
240pub enum Qualify {
241    #[default]
242    Compat,
243    Extend,
244}
245
246/// What a READ of a variable-length record checks its length against: the level-01 records under
247/// `Standard`, RECORD VARYING under `Compat`; outside them its status is 04, else 00 (Programming
248/// Guide SC27-8714-03, pp. 422-424, Table 52). See assumptions C218 and C219.
249#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
250pub enum Vlr {
251    #[default]
252    Standard,
253    Compat,
254}
255
256/// The file status of a VSAM OPEN that succeeds once its file's integrity is verified: 97 under
257/// `Compat`, 00 under `Succ` (Programming Guide SC27-8714-03, p. 424). No ironwork OPEN verifies
258/// a file yet (assumption C220).
259#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
260pub enum VsamOpenFs {
261    #[default]
262    Compat,
263    Succ,
264}
265
266/// Whether a program whose compile gave warnings runs (`Proceed`), or (`-warnings-block`, the
267/// command line's NOCOMPILE(W)) is refused. The return code is 4 either way.
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub enum Warnings {
270    #[default]
271    Proceed,
272    Block,
273}
274
275/// The COMPILE option: which messages stop the object code, so that run and cics refuse the
276/// program (Programming Guide SC27-8714-03, p. 355).
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum Compile {
279    /// COMPILE: object code whatever the messages, unless one is U and the compilation ended.
280    Full,
281    /// NOCOMPILE(W), NOCOMPILE(E) or NOCOMPILE(S): none from the first message of that severity up.
282    Until(Stop),
283    /// NOCOMPILE: a syntax check, with no object code at all.
284    SyntaxOnly,
285}
286
287/// A severity NOCOMPILE names.
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum Stop {
290    W,
291    E,
292    S,
293}
294
295impl Default for Compile {
296    fn default() -> Self {
297        Self::Until(Stop::S)
298    }
299}
300
301impl Compile {
302    /// The lowest return code (Programming Guide Table 38, p. 282) of a message that stops the object
303    /// code: 0 stops it whatever the messages.
304    pub const fn stops_at(self) -> u8 {
305        match self {
306            Self::Full => 16,
307            Self::Until(Stop::S) => 12,
308            Self::Until(Stop::E) => 8,
309            Self::Until(Stop::W) => 4,
310            Self::SyntaxOnly => 0,
311        }
312    }
313}
314
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub struct Options {
317    pub arith: Arith,
318    pub trunc: Trunc,
319    pub numproc: Numproc,
320    pub codepage: u16,
321    pub trunc_check: TruncCheck,
322    /// FASTSRT: DFSORT does the I/O of a SORT's USING and GIVING files where IBM's rules allow.
323    pub fastsrt: bool,
324    pub fastsrt_adv_print: FastsrtAdvPrint,
325    pub sort_keys: SortKeys,
326    /// ADV: a print file's printer control character is a byte added before each record; under
327    /// NOADV it is the record's own first byte.
328    pub adv: bool,
329    /// THREAD, DLL, RENT and DBCS, which object-oriented programs are compiled with.
330    pub thread: bool,
331    pub dll: bool,
332    pub rent: bool,
333    pub dbcs: bool,
334    pub warnings: Warnings,
335    /// COMPILE or NOCOMPILE as a CBL or PROCESS card gave it; None when none did.
336    pub compile: Option<Compile>,
337    /// DYNAM: a CALL of a literal loads the program at run time, as a CALL of an identifier does.
338    pub dynam: bool,
339    /// The Language Environment runtime option DEBUG (`-debug`): USE FOR DEBUGGING procedures run.
340    /// NODEBUG, IBM's default, keeps them from running (assumption C63).
341    pub debug: bool,
342    pub cics_return_warning: CicsReturnWarning,
343    /// INVDATA's suboptions; None for NOINVDATA, IBM's default, which assumes the data is valid.
344    pub invdata: Option<Invdata>,
345    /// ZWB: a signed zoned item compared with a nonnumeric operand loses its sign first.
346    pub zwb: bool,
347    pub quote: Quote,
348    /// CURRENCY(literal): the currency symbol a program with no CURRENCY SIGN clause uses in place
349    /// of $. None under NOCURRENCY, IBM's default.
350    pub currency: Option<Currency>,
351    pub nsymbol: Nsymbol,
352    pub dispsign: DispSign,
353    pub intdate: IntDate,
354    pub qualify: Qualify,
355    /// INITIAL: the program and its nested programs behave as though their PROGRAM-ID paragraphs
356    /// said IS INITIAL (Programming Guide SC27-8714-03, p. 374). A compile with THREAD takes
357    /// NOINITIAL (p. 344).
358    pub initial: bool,
359    pub vlr: Vlr,
360    pub vsamopenfs: VsamOpenFs,
361}
362
363impl Default for Options {
364    fn default() -> Self {
365        Self {
366            arith: Arith::default(),
367            trunc: Trunc::default(),
368            numproc: Numproc::default(),
369            codepage: 1140,
370            trunc_check: TruncCheck::default(),
371            fastsrt: false,
372            fastsrt_adv_print: FastsrtAdvPrint::default(),
373            sort_keys: SortKeys::default(),
374            adv: true,
375            thread: false,
376            dll: false,
377            rent: true,
378            dbcs: true,
379            warnings: Warnings::default(),
380            compile: None,
381            dynam: false,
382            debug: false,
383            cics_return_warning: CicsReturnWarning::default(),
384            invdata: None,
385            zwb: true,
386            quote: Quote::default(),
387            currency: None,
388            nsymbol: Nsymbol::default(),
389            dispsign: DispSign::default(),
390            intdate: IntDate::default(),
391            qualify: Qualify::default(),
392            initial: false,
393            vlr: Vlr::default(),
394            vsamopenfs: VsamOpenFs::default(),
395        }
396    }
397}
398
399/// What an option could not do. Each but UnsupportedCodePage and UnknownFlag is a message IBM's
400/// compiler gives and carries on from (assumptions C120 to C122).
401#[derive(Clone, Debug, PartialEq, Eq)]
402pub enum OptionError {
403    /// A suboption the option does not have: the option is discarded.
404    BadSuboption { option: String, given: String },
405    /// A suboption IBM removed, replaced by `instead`.
406    Removed { option: String, since: &'static str, instead: &'static str },
407    /// An option Enterprise COBOL 6.4 does not have, given a warning or (`warning` false) an
408    /// informational message and no effect.
409    NoEffect { option: &'static str, why: &'static str, warning: bool },
410    UnsupportedCodePage(u16),
411    UnknownFlag(String),
412}
413
414impl fmt::Display for OptionError {
415    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416        match self {
417            Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
418            Self::Removed { option, since, instead } => write!(f, "{option} was removed in Enterprise COBOL {since}, so {instead} is in effect"),
419            Self::NoEffect { option, why, .. } => write!(f, "{option} {why}"),
420            Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not a single-byte EBCDIC page this compiler carries"),
421            Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
422        }
423    }
424}
425
426/// The options IBM removed from Enterprise COBOL that a 6.4 compile accepts without effect, by
427/// spelling: LIB and SIZE (Migration Guide GC27-8715-03, Table 32, p. 167), FLAGSAA and NOFDUMP
428/// (Table 23, p. 112).
429fn without_effect(name: &str) -> Option<OptionError> {
430    let (option, why, warning) = match name {
431        "LIB" => ("LIB", "is no longer needed: COPY members are always read from the libraries", false),
432        "SIZE" | "SZ" => ("SIZE", "was removed in Enterprise COBOL V5 and has no effect", false),
433        "FLAGSAA" => ("FLAGSAA", "is not an Enterprise COBOL option and has no effect", true),
434        "NOFDUMP" => ("NOFDUMP", "is not an Enterprise COBOL option and has no effect", true),
435        _ => return None,
436    };
437    Some(OptionError::NoEffect { option, why, warning })
438}
439
440/// CURRENCY's literal: one character between quotation marks or apostrophes, or a hexadecimal
441/// literal of one byte (Programming Guide SC27-8714-03, p. 358). A figurative constant, a
442/// null-terminated, DBCS or national literal is none of these, so it is refused here.
443fn currency_literal(text: &str) -> Option<Currency> {
444    fn quoted(t: &str) -> Option<&str> {
445        let q = t.chars().next().filter(|q| matches!(q, '\'' | '"'))?;
446        (t.len() >= 2 && t.ends_with(q)).then(|| &t[1..t.len() - 1])
447    }
448    let text = text.trim();
449    if let Some(hex) = text.strip_prefix(['X', 'x']).and_then(quoted) {
450        return u8::from_str_radix(hex, 16).ok().filter(|_| hex.len() == 2).map(Currency::Hex);
451    }
452    let mut chars = quoted(text)?.chars();
453    match (chars.next(), chars.next()) {
454        (Some(c), None) if can_be_currency(c) => Some(Currency::Char(c)),
455        _ => None,
456    }
457}
458
459/// Whether the CURRENCY option may name `c`: a single-byte character that is no digit, space, one
460/// of the letters A B C D E G N P R S U V X Z in either case, or one of * + - / , . ; ( ) " =.
461pub fn can_be_currency(c: char) -> bool {
462    u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz *+-/,.;()\"=".contains(c)
463}
464
465impl std::error::Error for OptionError {}
466
467impl Options {
468    /// Applies one IBM compiler option as a CBL or PROCESS card or PARM writes it, e.g.
469    /// `TRUNC(OPT)`, `AR(E)`, `CP(1047)`. Returns false for an option this layer does not read. An
470    /// error leaves the options as they were, except NUMPROC(MIG), which sets the default NUMPROC.
471    pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
472        let given = option.trim();
473        let option = given.to_ascii_uppercase();
474        let (name, sub) = match option.split_once('(') {
475            Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
476            None => (option.as_str(), ""),
477        };
478        let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
479        if let Some(e) = without_effect(name) {
480            return Err(e);
481        }
482        let Some((documented, off)) = spelled(name) else { return Ok(false) };
483        match documented.name {
484            "ARITH" => {
485                self.arith = match sub {
486                    "COMPAT" | "C" => Arith::Compat,
487                    "EXTEND" | "E" => Arith::Extend,
488                    _ => return Err(bad()),
489                }
490            }
491            "TRUNC" => {
492                self.trunc = match sub {
493                    "STD" => Trunc::Std,
494                    "OPT" => Trunc::Opt,
495                    "BIN" => Trunc::Bin,
496                    _ => return Err(bad()),
497                }
498            }
499            "NUMPROC" => {
500                self.numproc = match sub {
501                    "NOPFD" => Numproc::Nopfd,
502                    "PFD" => Numproc::Pfd,
503                    "MIG" => {
504                        self.numproc = Numproc::default();
505                        return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5", instead: "NUMPROC(NOPFD)" });
506                    }
507                    _ => return Err(bad()),
508                }
509            }
510            "CODEPAGE" => {
511                let ccsid: u16 = sub.parse().map_err(|_| bad())?;
512                CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
513                self.codepage = ccsid;
514            }
515            "COMPILE" => {
516                self.compile = Some(match (off, sub) {
517                    (false, "") => Compile::Full,
518                    (true, "") => Compile::SyntaxOnly,
519                    (true, "W") => Compile::Until(Stop::W),
520                    (true, "E") => Compile::Until(Stop::E),
521                    (true, "S") => Compile::Until(Stop::S),
522                    _ => return Err(bad()),
523                })
524            }
525            "INVDATA" if off => self.invdata = None,
526            "INVDATA" => {
527                let mut invdata = Invdata::default();
528                for part in sub.split(',').map(str::trim).filter(|p| !p.is_empty()) {
529                    match part {
530                        "FORCENUMCMP" | "FNC" => invdata.forcenumcmp = true,
531                        "NOFORCENUMCMP" | "NOFNC" => invdata.forcenumcmp = false,
532                        "CLEANSIGN" | "CS" => invdata.cleansign = true,
533                        "NOCLEANSIGN" | "NOCS" => invdata.cleansign = false,
534                        _ => return Err(bad()),
535                    }
536                }
537                self.invdata = Some(invdata);
538            }
539            "ZONEDATA" => {
540                self.invdata = match sub {
541                    "PFD" => None,
542                    "NOPFD" => Some(Invdata::default()),
543                    "MIG" => Some(Invdata { forcenumcmp: true, cleansign: true }),
544                    _ => return Err(bad()),
545                }
546            }
547            "ZWB" => self.zwb = !off,
548            "FASTSRT" => self.fastsrt = !off,
549            "ADV" => self.adv = !off,
550            "THREAD" => self.thread = !off,
551            "DLL" => self.dll = !off,
552            "RENT" => self.rent = !off,
553            "DBCS" => self.dbcs = !off,
554            "DYNAM" => self.dynam = !off,
555            "APOST/QUOTE" if sub.is_empty() => self.quote = if name == "APOST" { Quote::Apost } else { Quote::Quote },
556            "CURRENCY" => {
557                self.currency = match (off, sub) {
558                    (true, "") => None,
559                    (false, _) => Some(currency_literal(given.split_once('(').map_or("", |(_, rest)| rest.trim_end().trim_end_matches(')'))).ok_or_else(bad)?),
560                    _ => return Err(bad()),
561                }
562            }
563            "NSYMBOL" => {
564                self.nsymbol = match sub {
565                    "NATIONAL" | "NAT" => Nsymbol::National,
566                    "DBCS" => Nsymbol::Dbcs,
567                    _ => return Err(bad()),
568                }
569            }
570            "DISPSIGN" => {
571                self.dispsign = match sub {
572                    "COMPAT" | "C" => DispSign::Compat,
573                    "SEP" | "S" => DispSign::Sep,
574                    _ => return Err(bad()),
575                }
576            }
577            "INTDATE" => {
578                self.intdate = match sub {
579                    "ANSI" => IntDate::Ansi,
580                    "LILIAN" => IntDate::Lilian,
581                    _ => return Err(bad()),
582                }
583            }
584            "QUALIFY" => {
585                self.qualify = match sub {
586                    "COMPAT" | "C" => Qualify::Compat,
587                    "EXTEND" | "E" => Qualify::Extend,
588                    _ => return Err(bad()),
589                }
590            }
591            "INITIAL" if sub.is_empty() => self.initial = !off,
592            "VLR" => {
593                self.vlr = match sub {
594                    "STANDARD" | "S" => Vlr::Standard,
595                    "COMPAT" | "C" => Vlr::Compat,
596                    _ => return Err(bad()),
597                }
598            }
599            "VSAMOPENFS" => {
600                self.vsamopenfs = match sub {
601                    "COMPAT" | "C" => VsamOpenFs::Compat,
602                    "SUCC" | "S" => VsamOpenFs::Succ,
603                    _ => return Err(bad()),
604                }
605            }
606            "APOST/QUOTE" | "INITIAL" => return Err(bad()),
607            _ => return Ok(false),
608        }
609        Ok(true)
610    }
611
612    /// Applies a flag of this compiler's own command line.
613    pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
614        match flag {
615            "-silent" => self.trunc_check = TruncCheck::Silent,
616            "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
617            "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
618            "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
619            "-warnings-block" => self.warnings = Warnings::Block,
620            "-debug" => self.debug = true,
621            "--cics-return-warning=once" => self.cics_return_warning = CicsReturnWarning::Once,
622            "--cics-return-warning=always" => self.cics_return_warning = CicsReturnWarning::Always,
623            "--cics-return-warning=never" => self.cics_return_warning = CicsReturnWarning::Never,
624            _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
625        }
626        Ok(())
627    }
628
629    /// The COMPILE option in force: a card's, which outranks the command line as a PROCESS
630    /// statement outranks the compiler's invocation (Programming Guide SC27-8714-03, p. 273), else
631    /// NOCOMPILE(W) under `-warnings-block`, else IBM's default NOCOMPILE(S).
632    pub fn object_code(&self) -> Compile {
633        self.compile.unwrap_or(match self.warnings {
634            Warnings::Block => Compile::Until(Stop::W),
635            Warnings::Proceed => Compile::default(),
636        })
637    }
638
639    pub fn code_page(&self) -> &'static CodePage {
640        CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
641    }
642
643    /// The CURRENCY option's character, a hexadecimal literal's read in the program's code page;
644    /// Err with that character when the page gives one the option may not name.
645    pub fn currency_symbol(&self) -> Option<Result<char, char>> {
646        self.currency.map(|c| match c {
647            Currency::Char(c) => Ok(c),
648            Currency::Hex(b) => {
649                let c = self.code_page().decode_byte(b);
650                if can_be_currency(c) { Ok(c) } else { Err(c) }
651            }
652        })
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn defaults_are_ibms() {
662        let o = Options::default();
663        assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
664        assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
665    }
666
667    #[test]
668    fn invdata_zonedata_and_zwb() {
669        let mut o = Options::default();
670        assert_eq!((o.invdata, o.zwb), (None, true));
671        assert_eq!(o.apply("INVDATA"), Ok(true));
672        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: false, cleansign: true }));
673        assert_eq!(o.apply("INVD(FNC,NOCS)"), Ok(true));
674        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: false }));
675        assert_eq!(o.apply("NOINVDATA"), Ok(true));
676        assert_eq!(o.invdata, None);
677        assert_eq!(o.apply("ZD(MIG)"), Ok(true));
678        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: true }));
679        assert_eq!(o.apply("ZONEDATA(NOPFD)"), Ok(true));
680        assert_eq!(o.invdata, Some(Invdata::default()));
681        assert_eq!(o.apply("ZONEDATA(PFD)"), Ok(true));
682        assert_eq!(o.invdata, None);
683        assert!(o.apply("INVDATA(SOMETIMES)").is_err());
684        assert_eq!(o.apply("NOZWB"), Ok(true));
685        assert!(!o.zwb);
686    }
687
688    #[test]
689    fn dynam_and_its_abbreviations() {
690        let mut o = Options::default();
691        assert_eq!(o.apply("DYN"), Ok(true));
692        assert!(o.dynam);
693        assert_eq!(o.apply("NODYNAM"), Ok(true));
694        assert!(!o.dynam);
695    }
696
697    #[test]
698    fn adv_and_noadv_have_no_abbreviations() {
699        let mut o = Options::default();
700        assert_eq!(o.apply("noadv"), Ok(true));
701        assert!(!o.adv);
702        assert_eq!(o.apply("ADV"), Ok(true));
703        assert!(o.adv);
704        assert_eq!(o.apply("NOAD"), Ok(false));
705    }
706
707    #[test]
708    fn thread_dll_rent_and_dbcs_and_their_negatives() {
709        let mut o = Options::default();
710        for option in ["thread", "DLL", "NORENT", "NODBCS"] {
711            assert_eq!(o.apply(option), Ok(true));
712        }
713        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
714        for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
715            assert_eq!(o.apply(option), Ok(true));
716        }
717        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
718    }
719
720    #[test]
721    fn abbreviations_and_case() {
722        let mut o = Options::default();
723        assert_eq!(o.apply("ar(e)"), Ok(true));
724        assert_eq!(o.apply("CP(1047)"), Ok(true));
725        assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
726        assert_eq!(o.apply("fsrt"), Ok(true));
727        assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
728        assert_eq!(o.apply("NOFASTSRT"), Ok(true));
729        assert!(!o.fastsrt);
730    }
731
732    #[test]
733    fn ibms_table_is_whole_and_no_spelling_names_two_options() {
734        let all: Vec<Documented> = documented().collect();
735        assert_eq!(all.len(), 85, "Table 45 lists 85 options");
736        let mut seen = std::collections::HashMap::new();
737        for o in &all {
738            for s in o.spellings() {
739                assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
740            }
741        }
742        assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
743        let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
744        assert!(!adata.process && adata.page == 345);
745    }
746
747    #[test]
748    fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
749        let suboption = |name| match name {
750            "ARITH" => "(E)",
751            "CODEPAGE" => "(1047)",
752            "TRUNC" => "(OPT)",
753            "NUMPROC" => "(PFD)",
754            "ZONEDATA" => "(MIG)",
755            _ => "",
756        };
757        for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE", "INVDATA", "ZONEDATA", "ZWB"] {
758            let o = documented().find(|o| o.name == name).unwrap();
759            for s in o.spellings() {
760                assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
761            }
762        }
763        let mut o = Options::default();
764        o.apply("FSRT").unwrap();
765        o.apply("NOFSRT").unwrap();
766        assert!(!o.fastsrt);
767        assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
768        assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
769        assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
770        assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
771    }
772
773    /// Run with IRONWORK_COBOLWORK_DIR naming a cobolwork checkout to check the vendored copy.
774    #[test]
775    fn the_vendored_table_is_cobolworks() {
776        let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
777        let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
778        assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
779    }
780
781    #[test]
782    fn options_this_layer_does_not_read_pass_through() {
783        assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
784    }
785
786    #[test]
787    fn an_option_that_cannot_be_applied_says_why_and_leaves_the_options_as_they_were() {
788        let mut o = Options::default();
789        o.apply("NUMPROC(PFD)").unwrap();
790        o.apply("TRUNC(BIN)").unwrap();
791        assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
792        assert_eq!(o.apply("CODEPAGE(930)"), Err(OptionError::UnsupportedCodePage(930)));
793        assert!(matches!(o.apply("CP(X)"), Err(OptionError::BadSuboption { .. })));
794        for (option, warns) in [("LIB", false), ("SIZE(MAX)", false), ("sz(2097152)", false), ("FLAGSAA", true), ("NOFDUMP", true)] {
795            assert!(matches!(o.apply(option), Err(OptionError::NoEffect { warning, .. }) if warning == warns), "{option}");
796        }
797        assert_eq!((o.apply("NOLIB"), o.apply("FDUMP")), (Ok(false), Ok(false)));
798        assert_eq!((o.numproc, o.trunc, o.codepage), (Numproc::Pfd, Trunc::Bin, 1140));
799        let removed = o.apply("NUMPROC(MIG)").unwrap_err();
800        assert_eq!(removed.to_string(), "NUMPROC(MIG) was removed in Enterprise COBOL V5, so NUMPROC(NOPFD) is in effect");
801        assert_eq!(o.numproc, Numproc::Nopfd, "the default NUMPROC, not the one before");
802    }
803
804    #[test]
805    fn silent_flag_turns_off_trunc_reports() {
806        let mut o = Options::default();
807        o.apply_flag("-silent").unwrap();
808        assert_eq!(o.trunc_check, TruncCheck::Silent);
809        o.apply_flag("-strict-sort-keys").unwrap();
810        assert_eq!(o.sort_keys, SortKeys::Strict);
811        assert!(o.apply_flag("-quiet").is_err());
812    }
813
814    #[test]
815    fn warnings_proceed_unless_the_flag_blocks_them() {
816        let mut o = Options::default();
817        assert_eq!(o.warnings, Warnings::Proceed);
818        o.apply_flag("-warnings-block").unwrap();
819        assert_eq!(o.warnings, Warnings::Block);
820        assert!(o.apply_flag("-Werror").is_err());
821    }
822
823    #[test]
824    fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
825        let given = |option: &str| {
826            let mut o = Options::default();
827            o.apply(option).map(|_| o.compile)
828        };
829        assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
830        assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
831        assert_eq!(given("c"), Ok(Some(Compile::Full)));
832        assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
833        assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
834        assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
835        assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
836        for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
837            assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
838        }
839        let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
840        assert_eq!(codes, [16, 12, 8, 4, 0]);
841    }
842
843    #[test]
844    fn a_card_outranks_warnings_block() {
845        let mut o = Options::default();
846        o.apply_flag("-warnings-block").unwrap();
847        assert_eq!(o.object_code(), Compile::Until(Stop::W));
848        o.apply("NOCOMPILE(S)").unwrap();
849        assert_eq!(o.object_code(), Compile::Until(Stop::S));
850        o.apply("NOCOMPILE(E)").unwrap();
851        assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
852    }
853
854    #[test]
855    fn fastsrt_adv_print_excludes_unless_included() {
856        let mut o = Options::default();
857        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
858        o.apply_flag("--fastsrt-adv-print=include").unwrap();
859        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
860        o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
861        assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
862        assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
863        assert!(o.apply_flag("--fastsrt-adv-print").is_err());
864    }
865
866    #[test]
867    fn the_options_of_roadmap_2_10_to_2_12_default_to_ibms_and_take_each_documented_suboption() {
868        let o = Options::default();
869        assert_eq!((o.quote, o.currency, o.nsymbol, o.dispsign, o.intdate), (Quote::Quote, None, Nsymbol::National, DispSign::Compat, IntDate::Ansi));
870        assert_eq!((o.qualify, o.initial, o.vlr, o.vsamopenfs), (Qualify::Compat, false, Vlr::Standard, VsamOpenFs::Compat));
871        let given = |cards: &[&str]| {
872            let mut o = Options::default();
873            cards.iter().for_each(|c| assert_eq!(o.apply(c), Ok(true), "{c}"));
874            o
875        };
876        assert_eq!(given(&["APOST"]).quote, Quote::Apost);
877        assert_eq!(given(&["apost", "Q"]).quote, Quote::Quote);
878        assert_eq!(given(&["NS(DBCS)"]).nsymbol, Nsymbol::Dbcs);
879        assert_eq!(given(&["NSYMBOL(DBCS)", "NS(NAT)"]).nsymbol, Nsymbol::National);
880        assert_eq!(given(&["DS(S)"]).dispsign, DispSign::Sep);
881        assert_eq!(given(&["DISPSIGN(SEP)", "DISPSIGN(COMPAT)"]).dispsign, DispSign::Compat);
882        assert_eq!(given(&["INTDATE(LILIAN)"]).intdate, IntDate::Lilian);
883        assert_eq!(given(&["QUA(E)"]).qualify, Qualify::Extend);
884        assert_eq!(given(&["QUALIFY(EXTEND)", "QUA(C)"]).qualify, Qualify::Compat);
885        assert!(given(&["INITIAL"]).initial);
886        assert!(!given(&["INITIAL", "NOINITIAL"]).initial);
887        assert_eq!(given(&["VLR(C)"]).vlr, Vlr::Compat);
888        assert_eq!(given(&["VLR(COMPAT)", "VLR(STANDARD)"]).vlr, Vlr::Standard);
889        assert_eq!(given(&["VS(S)"]).vsamopenfs, VsamOpenFs::Succ);
890        assert_eq!(given(&["VSAMOPENFS(SUCC)", "VSAMOPENFS(COMPAT)"]).vsamopenfs, VsamOpenFs::Compat);
891        for bad in ["APOST(X)", "NSYMBOL(N)", "DS(X)", "INTDATE(JULIAN)", "QUA(X)", "INITIAL(Y)", "VLR(X)", "VS(X)", "INTDATE"] {
892            assert!(matches!(Options::default().apply(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
893        }
894    }
895
896    #[test]
897    fn currency_takes_one_character_it_may_name_in_either_delimiter_or_a_hexadecimal_byte() {
898        let currency = |card: &str| {
899            let mut o = Options::default();
900            o.apply(card).map(|_| o.currency)
901        };
902        assert_eq!(currency("CURRENCY('£')"), Ok(Some(Currency::Char('£'))));
903        assert_eq!(currency("curr(\"f\")"), Ok(Some(Currency::Char('f'))), "the literal keeps its case");
904        assert_eq!(currency("CURRENCY(X'5B')"), Ok(Some(Currency::Hex(0x5B))));
905        assert_eq!(currency("NOCURR"), Ok(None));
906        for bad in ["CURRENCY('E')", "CURRENCY('e')", "CURRENCY('1')", "CURRENCY(' ')", "CURRENCY('*')", "CURRENCY('EUR')", "CURRENCY(SPACE)", "CURRENCY(N'£')", "CURRENCY(Z'£')", "CURRENCY(X'5B5B')", "CURRENCY", "NOCURRENCY('£')"] {
907            assert!(matches!(currency(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
908        }
909        let mut o = Options::default();
910        o.apply("CURRENCY(X'4A')").unwrap();
911        assert_eq!(o.currency_symbol(), Some(Ok('¢')), "X'4A' is the cent sign in CCSID 1140");
912        o.apply("CURRENCY(X'F1')").unwrap();
913        assert_eq!(o.currency_symbol(), Some(Err('1')));
914        assert_eq!(Options::default().currency_symbol(), None);
915    }
916
917    #[test]
918    fn cics_return_warning_is_once_unless_the_flag_says_always_or_never() {
919        let mut o = Options::default();
920        assert_eq!(o.cics_return_warning, CicsReturnWarning::Once);
921        for mode in [CicsReturnWarning::Always, CicsReturnWarning::Never, CicsReturnWarning::Once] {
922            o.apply_flag(mode.flag()).unwrap();
923            assert_eq!(o.cics_return_warning, mode);
924        }
925        assert!(o.apply_flag("--cics-return-warning=sometimes").is_err());
926        assert!(o.apply_flag("--cics-return-warning").is_err());
927    }
928}