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/// NUMCHECK: implicit numeric class tests of zoned and packed senders, and size tests of binary
154/// senders, each a warning that lets the statement run (MSG) or a terminating message (`abd`)
155/// (Programming Guide SC27-8714-03, pp. 388-392). ZONECHECK(MSG|ABD) is NUMCHECK(ZON,MSG|ABD)
156/// (p. 427).
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct Numcheck {
159    pub zon: Option<ZonCheck>,
160    pub pac: bool,
161    pub bin: Option<BinCheck>,
162    pub abd: bool,
163}
164
165impl Default for Numcheck {
166    fn default() -> Self {
167        Self { zon: Some(ZonCheck::default()), pac: true, bin: Some(BinCheck { truncbin: true }), abd: false }
168    }
169}
170
171/// ZON's suboptions: whether a zoned item compared with an alphanumeric operand is checked
172/// (ALPHNUM, the default), and whether the redefinitions p. 390 lists are tolerated (LAX).
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub struct ZonCheck {
175    pub alphnum: bool,
176    pub lax: bool,
177}
178
179impl Default for ZonCheck {
180    fn default() -> Self {
181        Self { alphnum: true, lax: false }
182    }
183}
184
185/// BIN's suboption: whether binary senders are checked under TRUNC(BIN) too (TRUNCBIN, the default).
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct BinCheck {
188    pub truncbin: bool,
189}
190
191/// PARMCHECK: a buffer of `bytes` after the last WORKING-STORAGE item, set to X'AA' before each
192/// CALL and checked after it, a change being a warning (MSG) or a terminating message (`abd`)
193/// (Programming Guide SC27-8714-03, p. 397).
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct Parmcheck {
196    pub abd: bool,
197    pub bytes: u16,
198}
199
200/// INITCHECK: a compile-time warning for a WORKING-STORAGE or LOCAL-STORAGE item used before it is
201/// set on some path (`Lax`, the default) or on any path (`Strict`) (Programming Guide
202/// SC27-8714-03, pp. 373-374).
203#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
204pub enum Initcheck {
205    #[default]
206    Lax,
207    Strict,
208}
209
210/// What a program with no STOP RUN, GOBACK or EXIT PROGRAM that ends with EXEC CICS RETURN or XCTL
211/// gets (assumption C124): IBM's warning (`--cics-return-warning=always`), one informational note
212/// in place of it (`once`), or nothing (`never`).
213#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub enum CicsReturnWarning {
215    #[default]
216    Once,
217    Always,
218    Never,
219}
220
221impl CicsReturnWarning {
222    pub const fn flag(self) -> &'static str {
223        match self {
224            Self::Once => "--cics-return-warning=once",
225            Self::Always => "--cics-return-warning=always",
226            Self::Never => "--cics-return-warning=never",
227        }
228    }
229}
230
231/// What the figurative constant QUOTE is: a quotation mark under QUOTE, IBM's default, or an
232/// apostrophe under APOST (Programming Guide SC27-8714-03, p. 347).
233#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
234pub enum Quote {
235    #[default]
236    Quote,
237    Apost,
238}
239
240impl Quote {
241    /// The character in EBCDIC, where every single-byte page carries it at the same place.
242    pub const fn byte(self) -> u8 {
243        match self {
244            Self::Quote => ebcdic::QUOTE,
245            Self::Apost => ebcdic::APOSTROPHE,
246        }
247    }
248
249    /// The character as a UTF-16 unit, for a national item.
250    pub const fn unit(self) -> u16 {
251        match self {
252            Self::Quote => 0x0022,
253            Self::Apost => 0x0027,
254        }
255    }
256}
257
258/// CURRENCY(literal): the character, or a hexadecimal literal's byte, which the program's code page
259/// turns into one (Programming Guide SC27-8714-03, p. 358).
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261pub enum Currency {
262    Char(char),
263    Hex(u8),
264}
265
266/// Whether N literals, and PICTURE N items with no USAGE clause, are national, IBM's default, or
267/// DBCS (Programming Guide SC27-8714-03, pp. 387-388).
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub enum Nsymbol {
270    #[default]
271    National,
272    Dbcs,
273}
274
275/// How DISPLAY shows a signed binary, packed or overpunched zoned item: as releases before 6 did,
276/// with an overpunched digit (`Compat`), or with a separate leading sign (`Sep`) (Programming
277/// Guide SC27-8714-03, pp. 362-363).
278#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
279pub enum DispSign {
280    #[default]
281    Compat,
282    Sep,
283}
284
285/// Day 1 of the date intrinsic functions' integer dates: 1 January 1601 (`Ansi`), or Language
286/// Environment's Lilian 15 October 1582 (Programming Guide SC27-8714-03, p. 375).
287#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
288pub enum IntDate {
289    #[default]
290    Ansi,
291    Lilian,
292}
293
294/// Whether a reference must be unique by the standard's rules (`Compat`), or resolves to the one
295/// item a complete set of qualifiers names (`Extend`) (Programming Guide SC27-8714-03, p. 400).
296#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
297pub enum Qualify {
298    #[default]
299    Compat,
300    Extend,
301}
302
303/// What a READ of a variable-length record checks its length against: the level-01 records under
304/// `Standard`, RECORD VARYING under `Compat`; outside them its status is 04, else 00 (Programming
305/// Guide SC27-8714-03, pp. 422-424, Table 52). See assumptions C218 and C219.
306#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
307pub enum Vlr {
308    #[default]
309    Standard,
310    Compat,
311}
312
313/// The file status of a VSAM OPEN that succeeds once its file's integrity is verified: 97 under
314/// `Compat`, 00 under `Succ` (Programming Guide SC27-8714-03, p. 424). OPEN verifies a data set a
315/// run left open for output (assumption C220).
316#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
317pub enum VsamOpenFs {
318    #[default]
319    Compat,
320    Succ,
321}
322
323/// Whether a program whose compile gave warnings runs (`Proceed`), or (`-warnings-block`, the
324/// command line's NOCOMPILE(W)) is refused. The return code is 4 either way.
325#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
326pub enum Warnings {
327    #[default]
328    Proceed,
329    Block,
330}
331
332/// Whose result a computation gives where ironwork knowingly differs from GnuCOBOL: Enterprise
333/// COBOL's as the assumptions register reads it (`Ibm`), or that of GnuCOBOL's `cobc -std=ibm`
334/// (`Gnucobol`, `--dialect gnucobol`), so a migration can compare ironwork with a GnuCOBOL build.
335/// docs/dialect.md lists each difference.
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
337pub enum Dialect {
338    #[default]
339    Ibm,
340    Gnucobol,
341}
342
343impl Dialect {
344    pub const fn flag(self) -> &'static str {
345        match self {
346            Self::Ibm => "--dialect=ibm",
347            Self::Gnucobol => "--dialect=gnucobol",
348        }
349    }
350
351    /// The value `--dialect` takes for it.
352    pub const fn name(self) -> &'static str {
353        match self {
354            Self::Ibm => "ibm",
355            Self::Gnucobol => "gnucobol",
356        }
357    }
358
359    pub fn named(value: &str) -> Option<Self> {
360        [Self::Ibm, Self::Gnucobol].into_iter().find(|d| d.name() == value)
361    }
362}
363
364/// The COMPILE option: which messages stop the object code, so that run and cics refuse the
365/// program (Programming Guide SC27-8714-03, p. 355).
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub enum Compile {
368    /// COMPILE: object code whatever the messages, unless one is U and the compilation ended.
369    Full,
370    /// NOCOMPILE(W), NOCOMPILE(E) or NOCOMPILE(S): none from the first message of that severity up.
371    Until(Stop),
372    /// NOCOMPILE: a syntax check, with no object code at all.
373    SyntaxOnly,
374}
375
376/// A severity NOCOMPILE names.
377#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378pub enum Stop {
379    W,
380    E,
381    S,
382}
383
384impl Default for Compile {
385    fn default() -> Self {
386        Self::Until(Stop::S)
387    }
388}
389
390impl Compile {
391    /// The lowest return code (Programming Guide Table 38, p. 282) of a message that stops the object
392    /// code: 0 stops it whatever the messages.
393    pub const fn stops_at(self) -> u8 {
394        match self {
395            Self::Full => 16,
396            Self::Until(Stop::S) => 12,
397            Self::Until(Stop::E) => 8,
398            Self::Until(Stop::W) => 4,
399            Self::SyntaxOnly => 0,
400        }
401    }
402}
403
404/// Whether the compile refuses what Enterprise COBOL refuses (`Strict`), or accepts, each with a
405/// warning, the other dialects' extensions docs/compliance.md lists (`Extended`,
406/// `--compliance extended`).
407#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
408pub enum Compliance {
409    #[default]
410    Strict,
411    Extended,
412}
413
414impl Compliance {
415    pub const fn flag(self) -> &'static str {
416        match self {
417            Self::Strict => "--compliance=strict",
418            Self::Extended => "--compliance=extended",
419        }
420    }
421
422    /// The value `--compliance` takes for it.
423    pub const fn name(self) -> &'static str {
424        match self {
425            Self::Strict => "strict",
426            Self::Extended => "extended",
427        }
428    }
429
430    pub fn named(value: &str) -> Option<Self> {
431        [Self::Strict, Self::Extended].into_iter().find(|c| c.name() == value)
432    }
433
434    /// The level the last `--compliance=` flag among `flags` gives, strict without one.
435    pub fn of(flags: &[String]) -> Self {
436        flags.iter().rev().find_map(|f| f.strip_prefix("--compliance=").and_then(Self::named)).unwrap_or_default()
437    }
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
441pub struct Options {
442    pub arith: Arith,
443    pub trunc: Trunc,
444    pub numproc: Numproc,
445    pub codepage: u16,
446    pub trunc_check: TruncCheck,
447    /// FASTSRT: DFSORT does the I/O of a SORT's USING and GIVING files where IBM's rules allow.
448    pub fastsrt: bool,
449    pub fastsrt_adv_print: FastsrtAdvPrint,
450    pub sort_keys: SortKeys,
451    /// ADV: a print file's printer control character is a byte added before each record; under
452    /// NOADV it is the record's own first byte.
453    pub adv: bool,
454    /// THREAD, DLL, RENT and DBCS, which object-oriented programs are compiled with.
455    pub thread: bool,
456    pub dll: bool,
457    pub rent: bool,
458    pub dbcs: bool,
459    pub warnings: Warnings,
460    /// COMPILE or NOCOMPILE as a CBL or PROCESS card gave it; None when none did.
461    pub compile: Option<Compile>,
462    /// DYNAM: a CALL of a literal loads the program at run time, as a CALL of an identifier does.
463    pub dynam: bool,
464    /// The Language Environment runtime option DEBUG (`-debug`): USE FOR DEBUGGING procedures run.
465    /// NODEBUG, IBM's default, keeps them from running (assumption C63).
466    pub debug: bool,
467    pub cics_return_warning: CicsReturnWarning,
468    /// INVDATA's suboptions; None for NOINVDATA, IBM's default, which assumes the data is valid.
469    pub invdata: Option<Invdata>,
470    /// ZWB: a signed zoned item compared with a nonnumeric operand loses its sign first.
471    pub zwb: bool,
472    pub quote: Quote,
473    /// CURRENCY(literal): the currency symbol a program with no CURRENCY SIGN clause uses in place
474    /// of $. None under NOCURRENCY, IBM's default.
475    pub currency: Option<Currency>,
476    pub nsymbol: Nsymbol,
477    pub dispsign: DispSign,
478    pub intdate: IntDate,
479    pub qualify: Qualify,
480    /// INITIAL: the program and its nested programs behave as though their PROGRAM-ID paragraphs
481    /// said IS INITIAL (Programming Guide SC27-8714-03, p. 374). A compile with THREAD takes
482    /// NOINITIAL (p. 344).
483    pub initial: bool,
484    pub vlr: Vlr,
485    pub vsamopenfs: VsamOpenFs,
486    pub numcheck: Option<Numcheck>,
487    pub parmcheck: Option<Parmcheck>,
488    pub initcheck: Option<Initcheck>,
489    /// OPTIMIZE's level, 0 to 2.
490    pub optimize: u8,
491    pub compliance: Compliance,
492    pub dialect: Dialect,
493}
494
495impl Default for Options {
496    fn default() -> Self {
497        Self {
498            arith: Arith::default(),
499            trunc: Trunc::default(),
500            numproc: Numproc::default(),
501            codepage: 1140,
502            trunc_check: TruncCheck::default(),
503            fastsrt: false,
504            fastsrt_adv_print: FastsrtAdvPrint::default(),
505            sort_keys: SortKeys::default(),
506            adv: true,
507            thread: false,
508            dll: false,
509            rent: true,
510            dbcs: true,
511            warnings: Warnings::default(),
512            compile: None,
513            dynam: false,
514            debug: false,
515            cics_return_warning: CicsReturnWarning::default(),
516            invdata: None,
517            zwb: true,
518            quote: Quote::default(),
519            currency: None,
520            nsymbol: Nsymbol::default(),
521            dispsign: DispSign::default(),
522            intdate: IntDate::default(),
523            qualify: Qualify::default(),
524            initial: false,
525            vlr: Vlr::default(),
526            vsamopenfs: VsamOpenFs::default(),
527            numcheck: None,
528            parmcheck: None,
529            initcheck: None,
530            optimize: 0,
531            compliance: Compliance::default(),
532            dialect: Dialect::default(),
533        }
534    }
535}
536
537/// What an option could not do. Each but UnsupportedCodePage and UnknownFlag is a message IBM's
538/// compiler gives and carries on from (assumptions C120 to C122).
539#[derive(Clone, Debug, PartialEq, Eq)]
540pub enum OptionError {
541    /// A suboption the option does not have: the option is discarded.
542    BadSuboption { option: String, given: String },
543    /// A suboption IBM removed, replaced by `instead`.
544    Removed { option: String, since: &'static str, instead: &'static str },
545    /// An option Enterprise COBOL 6.4 does not have, given a warning or (`warning` false) an
546    /// informational message and no effect.
547    NoEffect { option: &'static str, why: &'static str, warning: bool },
548    UnsupportedCodePage(u16),
549    UnknownFlag(String),
550}
551
552impl fmt::Display for OptionError {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        match self {
555            Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
556            Self::Removed { option, since, instead } => write!(f, "{option} was removed in Enterprise COBOL {since}, so {instead} is in effect"),
557            Self::NoEffect { option, why, .. } => write!(f, "{option} {why}"),
558            Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not an EBCDIC page this compiler carries"),
559            Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
560        }
561    }
562}
563
564/// The options IBM removed from Enterprise COBOL that a 6.4 compile accepts without effect, by
565/// spelling: LIB and SIZE (Migration Guide GC27-8715-03, Table 32, p. 167), FLAGSAA and NOFDUMP
566/// (Table 23, p. 112).
567fn without_effect(name: &str) -> Option<OptionError> {
568    let (option, why, warning) = match name {
569        "LIB" => ("LIB", "is no longer needed: COPY members are always read from the libraries", false),
570        "SIZE" | "SZ" => ("SIZE", "was removed in Enterprise COBOL V5 and has no effect", false),
571        "FLAGSAA" => ("FLAGSAA", "is not an Enterprise COBOL option and has no effect", true),
572        "NOFDUMP" => ("NOFDUMP", "is not an Enterprise COBOL option and has no effect", true),
573        _ => return None,
574    };
575    Some(OptionError::NoEffect { option, why, warning })
576}
577
578/// CURRENCY's literal: one character between quotation marks or apostrophes, or a hexadecimal
579/// literal of one byte (Programming Guide SC27-8714-03, p. 358). A figurative constant, a
580/// null-terminated, DBCS or national literal is none of these, so it is refused here.
581fn currency_literal(text: &str) -> Option<Currency> {
582    fn quoted(t: &str) -> Option<&str> {
583        let q = t.chars().next().filter(|q| matches!(q, '\'' | '"'))?;
584        (t.len() >= 2 && t.ends_with(q)).then(|| &t[1..t.len() - 1])
585    }
586    let text = text.trim();
587    if let Some(hex) = text.strip_prefix(['X', 'x']).and_then(quoted) {
588        return u8::from_str_radix(hex, 16).ok().filter(|_| hex.len() == 2).map(Currency::Hex);
589    }
590    let mut chars = quoted(text)?.chars();
591    match (chars.next(), chars.next()) {
592        (Some(c), None) if can_be_currency(c) => Some(Currency::Char(c)),
593        _ => None,
594    }
595}
596
597/// The text between an option's first opening parenthesis and its last closing one.
598fn inner(option: &str) -> &str {
599    option.split_once('(').map_or("", |(_, rest)| rest.trim_end().strip_suffix(')').unwrap_or(rest)).trim()
600}
601
602/// `text` split at the commas outside parentheses.
603fn top_level(text: &str) -> Vec<&str> {
604    let (mut parts, mut depth, mut start) = (Vec::new(), 0i32, 0);
605    for (i, c) in text.char_indices() {
606        match c {
607            '(' => depth += 1,
608            ')' => depth -= 1,
609            ',' if depth == 0 => {
610                parts.push(text[start..i].trim());
611                start = i + 1;
612            }
613            _ => {}
614        }
615    }
616    parts.push(text[start..].trim());
617    parts.into_iter().filter(|p| !p.is_empty()).collect()
618}
619
620/// NUMCHECK's suboptions with IBM's defaults: none gives every data type and MSG; data types left
621/// out are on when none is given and off when any is; every data type off is NONUMCHECK, None
622/// (Programming Guide SC27-8714-03, p. 389). None inside Some is a suboption IBM does not have.
623fn numcheck(text: &str) -> Option<Option<Numcheck>> {
624    let (mut zon, mut pac, mut bin, mut abd, mut typed) = (None, None, None, false, false);
625    for part in top_level(text) {
626        let (word, args) = match part.split_once('(') {
627            Some((w, _)) => (w.trim(), top_level(inner(part))),
628            None => (part, Vec::new()),
629        };
630        match word {
631            "ZON" => {
632                let mut z = ZonCheck::default();
633                for a in args {
634                    match a {
635                        "ALPHNUM" => z.alphnum = true,
636                        "NOALPHNUM" => z.alphnum = false,
637                        "LAX" | "LAXREDEF" => z.lax = true,
638                        "STRICT" | "STRICTREDEF" => z.lax = false,
639                        _ => return None,
640                    }
641                }
642                zon = Some(Some(z));
643            }
644            "NOZON" if args.is_empty() => zon = Some(None),
645            "PAC" if args.is_empty() => pac = Some(true),
646            "NOPAC" if args.is_empty() => pac = Some(false),
647            "BIN" => {
648                bin = Some(Some(BinCheck {
649                    truncbin: match args[..] {
650                        [] | ["TRUNCBIN"] => true,
651                        ["NOTRUNCBIN"] => false,
652                        _ => return None,
653                    },
654                }))
655            }
656            "NOBIN" if args.is_empty() => bin = Some(None),
657            "MSG" if args.is_empty() => abd = false,
658            "ABD" if args.is_empty() => abd = true,
659            _ => return None,
660        }
661        typed |= matches!(word, "ZON" | "NOZON" | "PAC" | "NOPAC" | "BIN" | "NOBIN");
662    }
663    let all = Numcheck::default();
664    let n = if typed {
665        Numcheck { zon: zon.flatten(), pac: pac.unwrap_or(false), bin: bin.flatten(), abd }
666    } else {
667        Numcheck { abd, ..all }
668    };
669    Some((n.zon.is_some() || n.pac || n.bin.is_some()).then_some(n))
670}
671
672/// PARMCHECK's MSG or ABD and buffer size, 100 bytes and MSG when left out (Programming Guide
673/// SC27-8714-03, p. 397).
674fn parmcheck(sub: &str) -> Option<Parmcheck> {
675    let mut p = Parmcheck { abd: false, bytes: 100 };
676    let parts: Vec<&str> = sub.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
677    if parts.len() > 2 {
678        return None;
679    }
680    for (i, part) in parts.iter().enumerate() {
681        match *part {
682            "MSG" if i == 0 => p.abd = false,
683            "ABD" if i == 0 => p.abd = true,
684            n => p.bytes = n.parse().ok().filter(|b| (1..=9999).contains(b))?,
685        }
686    }
687    Some(p)
688}
689
690/// Whether the CURRENCY option may name `c`: a single-byte character that is no digit, space, one
691/// of the letters A B C D E G N P R S U V X Z in either case, or one of * + - / , . ; ( ) " =.
692pub fn can_be_currency(c: char) -> bool {
693    u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz *+-/,.;()\"=".contains(c)
694}
695
696impl std::error::Error for OptionError {}
697
698impl Options {
699    /// Applies one IBM compiler option as a CBL or PROCESS card or PARM writes it, e.g.
700    /// `TRUNC(OPT)`, `AR(E)`, `CP(1047)`. Returns false for an option this layer does not read. An
701    /// error leaves the options as they were, except NUMPROC(MIG), which sets the default NUMPROC.
702    pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
703        let given = option.trim();
704        let option = given.to_ascii_uppercase();
705        let (name, sub) = match option.split_once('(') {
706            Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
707            None => (option.as_str(), ""),
708        };
709        let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
710        if let Some(e) = without_effect(name) {
711            return Err(e);
712        }
713        let Some((documented, off)) = spelled(name) else { return Ok(false) };
714        match documented.name {
715            "ARITH" => {
716                self.arith = match sub {
717                    "COMPAT" | "C" => Arith::Compat,
718                    "EXTEND" | "E" => Arith::Extend,
719                    _ => return Err(bad()),
720                }
721            }
722            "TRUNC" => {
723                self.trunc = match sub {
724                    "STD" => Trunc::Std,
725                    "OPT" => Trunc::Opt,
726                    "BIN" => Trunc::Bin,
727                    _ => return Err(bad()),
728                }
729            }
730            "NUMPROC" => {
731                self.numproc = match sub {
732                    "NOPFD" => Numproc::Nopfd,
733                    "PFD" => Numproc::Pfd,
734                    "MIG" => {
735                        self.numproc = Numproc::default();
736                        return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5", instead: "NUMPROC(NOPFD)" });
737                    }
738                    _ => return Err(bad()),
739                }
740            }
741            "CODEPAGE" => {
742                let ccsid: u16 = sub.parse().map_err(|_| bad())?;
743                CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
744                self.codepage = ccsid;
745            }
746            "COMPILE" => {
747                self.compile = Some(match (off, sub) {
748                    (false, "") => Compile::Full,
749                    (true, "") => Compile::SyntaxOnly,
750                    (true, "W") => Compile::Until(Stop::W),
751                    (true, "E") => Compile::Until(Stop::E),
752                    (true, "S") => Compile::Until(Stop::S),
753                    _ => return Err(bad()),
754                })
755            }
756            "INVDATA" if off => self.invdata = None,
757            "INVDATA" => {
758                let mut invdata = Invdata::default();
759                for part in sub.split(',').map(str::trim).filter(|p| !p.is_empty()) {
760                    match part {
761                        "FORCENUMCMP" | "FNC" => invdata.forcenumcmp = true,
762                        "NOFORCENUMCMP" | "NOFNC" => invdata.forcenumcmp = false,
763                        "CLEANSIGN" | "CS" => invdata.cleansign = true,
764                        "NOCLEANSIGN" | "NOCS" => invdata.cleansign = false,
765                        _ => return Err(bad()),
766                    }
767                }
768                self.invdata = Some(invdata);
769            }
770            "ZONEDATA" => {
771                self.invdata = match sub {
772                    "PFD" => None,
773                    "NOPFD" => Some(Invdata::default()),
774                    "MIG" => Some(Invdata { forcenumcmp: true, cleansign: true }),
775                    _ => return Err(bad()),
776                }
777            }
778            "ZWB" => self.zwb = !off,
779            "NUMCHECK" if off => self.numcheck = None,
780            "NUMCHECK" => self.numcheck = numcheck(inner(&option)).ok_or_else(bad)?,
781            "ZONECHECK" if off => self.numcheck = self.numcheck.map(|n| Numcheck { zon: None, ..n }).filter(|n| n.pac || n.bin.is_some()),
782            "ZONECHECK" => {
783                let abd = match sub {
784                    "MSG" => false,
785                    "ABD" => true,
786                    _ => return Err(bad()),
787                };
788                self.numcheck = Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd });
789            }
790            "PARMCHECK" if off => self.parmcheck = None,
791            "PARMCHECK" => self.parmcheck = Some(parmcheck(sub).ok_or_else(bad)?),
792            "INITCHECK" if off => self.initcheck = None,
793            "INITCHECK" => {
794                self.initcheck = Some(match sub {
795                    "" | "LAX" => Initcheck::Lax,
796                    "STRICT" => Initcheck::Strict,
797                    _ => return Err(bad()),
798                })
799            }
800            "FASTSRT" => self.fastsrt = !off,
801            "ADV" => self.adv = !off,
802            "THREAD" => self.thread = !off,
803            "DLL" => self.dll = !off,
804            "RENT" => self.rent = !off,
805            "DBCS" => self.dbcs = !off,
806            "DYNAM" => self.dynam = !off,
807            "APOST/QUOTE" if sub.is_empty() => self.quote = if name == "APOST" { Quote::Apost } else { Quote::Quote },
808            "CURRENCY" => {
809                self.currency = match (off, sub) {
810                    (true, "") => None,
811                    (false, _) => Some(currency_literal(given.split_once('(').map_or("", |(_, rest)| rest.trim_end().trim_end_matches(')'))).ok_or_else(bad)?),
812                    _ => return Err(bad()),
813                }
814            }
815            "NSYMBOL" => {
816                self.nsymbol = match sub {
817                    "NATIONAL" | "NAT" => Nsymbol::National,
818                    "DBCS" => Nsymbol::Dbcs,
819                    _ => return Err(bad()),
820                }
821            }
822            "DISPSIGN" => {
823                self.dispsign = match sub {
824                    "COMPAT" | "C" => DispSign::Compat,
825                    "SEP" | "S" => DispSign::Sep,
826                    _ => return Err(bad()),
827                }
828            }
829            "INTDATE" => {
830                self.intdate = match sub {
831                    "ANSI" => IntDate::Ansi,
832                    "LILIAN" => IntDate::Lilian,
833                    _ => return Err(bad()),
834                }
835            }
836            "QUALIFY" => {
837                self.qualify = match sub {
838                    "COMPAT" | "C" => Qualify::Compat,
839                    "EXTEND" | "E" => Qualify::Extend,
840                    _ => return Err(bad()),
841                }
842            }
843            "INITIAL" if sub.is_empty() => self.initial = !off,
844            "VLR" => {
845                self.vlr = match sub {
846                    "STANDARD" | "S" => Vlr::Standard,
847                    "COMPAT" | "C" => Vlr::Compat,
848                    _ => return Err(bad()),
849                }
850            }
851            "VSAMOPENFS" => {
852                self.vsamopenfs = match sub {
853                    "COMPAT" | "C" => VsamOpenFs::Compat,
854                    "SUCC" | "S" => VsamOpenFs::Succ,
855                    _ => return Err(bad()),
856                }
857            }
858            // NOOPTIMIZE, OPTIMIZE, OPTIMIZE(STD) and OPTIMIZE(FULL) are tolerated as Table 51
859            // maps them; FULL's STGOPT changes nothing ironwork runs.
860            "OPTIMIZE" => {
861                self.optimize = match (off, sub) {
862                    (true, "") | (false, "0") => 0,
863                    (false, "1") => 1,
864                    (false, "2" | "" | "STD" | "FULL") => 2,
865                    _ => return Err(bad()),
866                }
867            }
868            "APOST/QUOTE" | "INITIAL" => return Err(bad()),
869            _ => return Ok(false),
870        }
871        Ok(true)
872    }
873
874    /// Applies a flag of this compiler's own command line.
875    pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
876        match flag {
877            "-silent" => self.trunc_check = TruncCheck::Silent,
878            "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
879            "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
880            "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
881            "-warnings-block" => self.warnings = Warnings::Block,
882            "-debug" => self.debug = true,
883            "--cics-return-warning=once" => self.cics_return_warning = CicsReturnWarning::Once,
884            "--cics-return-warning=always" => self.cics_return_warning = CicsReturnWarning::Always,
885            "--cics-return-warning=never" => self.cics_return_warning = CicsReturnWarning::Never,
886            // The compiler invocation's OPTIMIZE, which a card's outranks.
887            "--optimize=0" => self.optimize = 0,
888            "--optimize=1" => self.optimize = 1,
889            "--optimize=2" => self.optimize = 2,
890            f if f.starts_with("--compliance=") => match Compliance::named(&f["--compliance=".len()..]) {
891                Some(c) => self.compliance = c,
892                None => return Err(OptionError::UnknownFlag(flag.to_owned())),
893            },
894            f if f.starts_with("--dialect=") => match Dialect::named(&f["--dialect=".len()..]) {
895                Some(d) => self.dialect = d,
896                None => return Err(OptionError::UnknownFlag(flag.to_owned())),
897            },
898            _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
899        }
900        Ok(())
901    }
902
903    /// The COMPILE option in force: a card's, which outranks the command line as a PROCESS
904    /// statement outranks the compiler's invocation (Programming Guide SC27-8714-03, p. 273), else
905    /// NOCOMPILE(W) under `-warnings-block`, else IBM's default NOCOMPILE(S).
906    pub fn object_code(&self) -> Compile {
907        self.compile.unwrap_or(match self.warnings {
908            Warnings::Block => Compile::Until(Stop::W),
909            Warnings::Proceed => Compile::default(),
910        })
911    }
912
913    /// Whether an unsigned zoned integer compared with zero, or with an unsigned zoned integer of
914    /// its own length, is compared by its bytes, zones included: under INVDATA(NOFORCENUMCMP)
915    /// (assumption C223), and under NOINVDATA at OPTIMIZE(1) or OPTIMIZE(2) (C262).
916    pub fn zones_compared(&self) -> bool {
917        match self.invdata {
918            Some(i) => !i.forcenumcmp,
919            None => self.optimize > 0,
920        }
921    }
922
923    /// Whether an unsigned zoned integer compared with zero is compared by its bytes: where zones
924    /// are compared, but under NOINVDATA and --dialect gnucobol as a number at every OPTIMIZE
925    /// level, as cobc compares it (C262).
926    pub fn zones_compared_with_zero(&self) -> bool {
927        self.zones_compared() && !self.cobc_zoned_compare()
928    }
929
930    /// Whether an unsigned zoned integer compared with one of its own length is compared by its
931    /// bytes: where zones are compared, and under NOINVDATA and --dialect gnucobol at every
932    /// OPTIMIZE level, as cobc compares two such items with memcmp (C262).
933    pub fn zones_compared_between_items(&self) -> bool {
934        self.zones_compared() || self.cobc_zoned_compare()
935    }
936
937    fn cobc_zoned_compare(&self) -> bool {
938        self.invdata.is_none() && self.dialect == Dialect::Gnucobol
939    }
940
941    pub fn code_page(&self) -> &'static CodePage {
942        CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
943    }
944
945    /// The CURRENCY option's character, a hexadecimal literal's read in the program's code page;
946    /// Err with that character when the page gives one the option may not name.
947    pub fn currency_symbol(&self) -> Option<Result<char, char>> {
948        self.currency.map(|c| match c {
949            Currency::Char(c) => Ok(c),
950            Currency::Hex(b) => {
951                let c = self.code_page().decode_byte(b);
952                if can_be_currency(c) { Ok(c) } else { Err(c) }
953            }
954        })
955    }
956}
957
958#[cfg(test)]
959mod tests {
960    use super::*;
961
962    #[test]
963    fn defaults_are_ibms() {
964        let o = Options::default();
965        assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
966        assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
967    }
968
969    #[test]
970    fn invdata_zonedata_and_zwb() {
971        let mut o = Options::default();
972        assert_eq!((o.invdata, o.zwb), (None, true));
973        assert_eq!(o.apply("INVDATA"), Ok(true));
974        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: false, cleansign: true }));
975        assert_eq!(o.apply("INVD(FNC,NOCS)"), Ok(true));
976        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: false }));
977        assert_eq!(o.apply("NOINVDATA"), Ok(true));
978        assert_eq!(o.invdata, None);
979        assert_eq!(o.apply("ZD(MIG)"), Ok(true));
980        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: true }));
981        assert_eq!(o.apply("ZONEDATA(NOPFD)"), Ok(true));
982        assert_eq!(o.invdata, Some(Invdata::default()));
983        assert_eq!(o.apply("ZONEDATA(PFD)"), Ok(true));
984        assert_eq!(o.invdata, None);
985        assert!(o.apply("INVDATA(SOMETIMES)").is_err());
986        assert_eq!(o.apply("NOZWB"), Ok(true));
987        assert!(!o.zwb);
988    }
989
990    #[test]
991    fn optimize_levels_and_the_removed_spellings_table_51_maps() {
992        let mut o = Options::default();
993        assert_eq!(o.optimize, 0);
994        for (option, level) in [("OPT(1)", 1), ("OPTIMIZE(0)", 0), ("opt(2)", 2), ("NOOPTIMIZE", 0), ("OPTIMIZE", 2), ("NOOPTIMIZE", 0), ("OPTIMIZE(STD)", 2), ("OPT(0)", 0), ("OPTIMIZE(FULL)", 2)] {
995            assert_eq!(o.apply(option), Ok(true), "{option}");
996            assert_eq!(o.optimize, level, "{option}");
997        }
998        assert!(o.apply("OPT(3)").is_err());
999        assert!(o.apply("NOOPTIMIZE(2)").is_err());
1000        assert_eq!(o.optimize, 2);
1001    }
1002
1003    #[test]
1004    fn zones_are_compared_under_invdata_noforcenumcmp_or_noinvdata_optimized() {
1005        let compared = |options: &[&str]| {
1006            let mut o = Options::default();
1007            options.iter().for_each(|x| assert_eq!(o.apply(x), Ok(true)));
1008            o.zones_compared()
1009        };
1010        assert!(!compared(&[]));
1011        assert!(compared(&["OPT(1)"]));
1012        assert!(compared(&["OPT(2)"]));
1013        assert!(compared(&["INVDATA"]));
1014        assert!(!compared(&["INVDATA(FNC)", "OPT(2)"]));
1015        assert!(!compared(&["ZONEDATA(MIG)", "OPT(2)"]));
1016    }
1017
1018    #[test]
1019    fn dynam_and_its_abbreviations() {
1020        let mut o = Options::default();
1021        assert_eq!(o.apply("DYN"), Ok(true));
1022        assert!(o.dynam);
1023        assert_eq!(o.apply("NODYNAM"), Ok(true));
1024        assert!(!o.dynam);
1025    }
1026
1027    #[test]
1028    fn adv_and_noadv_have_no_abbreviations() {
1029        let mut o = Options::default();
1030        assert_eq!(o.apply("noadv"), Ok(true));
1031        assert!(!o.adv);
1032        assert_eq!(o.apply("ADV"), Ok(true));
1033        assert!(o.adv);
1034        assert_eq!(o.apply("NOAD"), Ok(false));
1035    }
1036
1037    #[test]
1038    fn thread_dll_rent_and_dbcs_and_their_negatives() {
1039        let mut o = Options::default();
1040        for option in ["thread", "DLL", "NORENT", "NODBCS"] {
1041            assert_eq!(o.apply(option), Ok(true));
1042        }
1043        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
1044        for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
1045            assert_eq!(o.apply(option), Ok(true));
1046        }
1047        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
1048    }
1049
1050    #[test]
1051    fn abbreviations_and_case() {
1052        let mut o = Options::default();
1053        assert_eq!(o.apply("ar(e)"), Ok(true));
1054        assert_eq!(o.apply("CP(1047)"), Ok(true));
1055        assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
1056        assert_eq!(o.apply("fsrt"), Ok(true));
1057        assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
1058        assert_eq!(o.apply("NOFASTSRT"), Ok(true));
1059        assert!(!o.fastsrt);
1060    }
1061
1062    #[test]
1063    fn ibms_table_is_whole_and_no_spelling_names_two_options() {
1064        let all: Vec<Documented> = documented().collect();
1065        assert_eq!(all.len(), 85, "Table 45 lists 85 options");
1066        let mut seen = std::collections::HashMap::new();
1067        for o in &all {
1068            for s in o.spellings() {
1069                assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
1070            }
1071        }
1072        assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
1073        let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
1074        assert!(!adata.process && adata.page == 345);
1075    }
1076
1077    #[test]
1078    fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
1079        let suboption = |name| match name {
1080            "ARITH" => "(E)",
1081            "CODEPAGE" => "(1047)",
1082            "TRUNC" => "(OPT)",
1083            "NUMPROC" => "(PFD)",
1084            "ZONEDATA" => "(MIG)",
1085            _ => "",
1086        };
1087        for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE", "INVDATA", "ZONEDATA", "ZWB", "OPTIMIZE"] {
1088            let o = documented().find(|o| o.name == name).unwrap();
1089            for s in o.spellings() {
1090                assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
1091            }
1092        }
1093        let mut o = Options::default();
1094        o.apply("FSRT").unwrap();
1095        o.apply("NOFSRT").unwrap();
1096        assert!(!o.fastsrt);
1097        assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
1098        assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
1099        assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
1100        assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
1101    }
1102
1103    /// Run with IRONWORK_COBOLWORK_DIR naming a cobolwork checkout to check the vendored copy.
1104    #[test]
1105    fn the_vendored_table_is_cobolworks() {
1106        let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
1107        let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
1108        assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
1109    }
1110
1111    #[test]
1112    fn options_this_layer_does_not_read_pass_through() {
1113        assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
1114    }
1115
1116    #[test]
1117    fn an_option_that_cannot_be_applied_says_why_and_leaves_the_options_as_they_were() {
1118        let mut o = Options::default();
1119        o.apply("NUMPROC(PFD)").unwrap();
1120        o.apply("TRUNC(BIN)").unwrap();
1121        assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
1122        assert_eq!(o.apply("CODEPAGE(290)"), Err(OptionError::UnsupportedCodePage(290)));
1123        assert!(matches!(o.apply("CP(X)"), Err(OptionError::BadSuboption { .. })));
1124        for (option, warns) in [("LIB", false), ("SIZE(MAX)", false), ("sz(2097152)", false), ("FLAGSAA", true), ("NOFDUMP", true)] {
1125            assert!(matches!(o.apply(option), Err(OptionError::NoEffect { warning, .. }) if warning == warns), "{option}");
1126        }
1127        assert_eq!((o.apply("NOLIB"), o.apply("FDUMP")), (Ok(false), Ok(false)));
1128        assert_eq!((o.numproc, o.trunc, o.codepage), (Numproc::Pfd, Trunc::Bin, 1140));
1129        let removed = o.apply("NUMPROC(MIG)").unwrap_err();
1130        assert_eq!(removed.to_string(), "NUMPROC(MIG) was removed in Enterprise COBOL V5, so NUMPROC(NOPFD) is in effect");
1131        assert_eq!(o.numproc, Numproc::Nopfd, "the default NUMPROC, not the one before");
1132    }
1133
1134    #[test]
1135    fn codepage_takes_the_mixed_pages_dbcs_programs_compile_with() {
1136        let mut o = Options::default();
1137        for ccsid in [930, 939, 1390, 1399, 5026, 5035, 933, 1364, 935, 1388, 937] {
1138            o.apply(&format!("CODEPAGE({ccsid})")).unwrap();
1139            assert!(o.code_page().dbcs_ccsid().is_some(), "{ccsid}");
1140        }
1141    }
1142
1143    #[test]
1144    fn silent_flag_turns_off_trunc_reports() {
1145        let mut o = Options::default();
1146        o.apply_flag("-silent").unwrap();
1147        assert_eq!(o.trunc_check, TruncCheck::Silent);
1148        o.apply_flag("-strict-sort-keys").unwrap();
1149        assert_eq!(o.sort_keys, SortKeys::Strict);
1150        assert!(o.apply_flag("-quiet").is_err());
1151    }
1152
1153    #[test]
1154    fn warnings_proceed_unless_the_flag_blocks_them() {
1155        let mut o = Options::default();
1156        assert_eq!(o.warnings, Warnings::Proceed);
1157        o.apply_flag("-warnings-block").unwrap();
1158        assert_eq!(o.warnings, Warnings::Block);
1159        assert!(o.apply_flag("-Werror").is_err());
1160    }
1161
1162    #[test]
1163    fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
1164        let given = |option: &str| {
1165            let mut o = Options::default();
1166            o.apply(option).map(|_| o.compile)
1167        };
1168        assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
1169        assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
1170        assert_eq!(given("c"), Ok(Some(Compile::Full)));
1171        assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
1172        assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
1173        assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
1174        assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
1175        for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
1176            assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1177        }
1178        let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
1179        assert_eq!(codes, [16, 12, 8, 4, 0]);
1180    }
1181
1182    #[test]
1183    fn a_card_outranks_warnings_block() {
1184        let mut o = Options::default();
1185        o.apply_flag("-warnings-block").unwrap();
1186        assert_eq!(o.object_code(), Compile::Until(Stop::W));
1187        o.apply("NOCOMPILE(S)").unwrap();
1188        assert_eq!(o.object_code(), Compile::Until(Stop::S));
1189        o.apply("NOCOMPILE(E)").unwrap();
1190        assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
1191    }
1192
1193    #[test]
1194    fn fastsrt_adv_print_excludes_unless_included() {
1195        let mut o = Options::default();
1196        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
1197        o.apply_flag("--fastsrt-adv-print=include").unwrap();
1198        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
1199        o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
1200        assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
1201        assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
1202        assert!(o.apply_flag("--fastsrt-adv-print").is_err());
1203    }
1204
1205    #[test]
1206    fn the_options_of_roadmap_2_10_to_2_12_default_to_ibms_and_take_each_documented_suboption() {
1207        let o = Options::default();
1208        assert_eq!((o.quote, o.currency, o.nsymbol, o.dispsign, o.intdate), (Quote::Quote, None, Nsymbol::National, DispSign::Compat, IntDate::Ansi));
1209        assert_eq!((o.qualify, o.initial, o.vlr, o.vsamopenfs), (Qualify::Compat, false, Vlr::Standard, VsamOpenFs::Compat));
1210        let given = |cards: &[&str]| {
1211            let mut o = Options::default();
1212            cards.iter().for_each(|c| assert_eq!(o.apply(c), Ok(true), "{c}"));
1213            o
1214        };
1215        assert_eq!(given(&["APOST"]).quote, Quote::Apost);
1216        assert_eq!(given(&["apost", "Q"]).quote, Quote::Quote);
1217        assert_eq!(given(&["NS(DBCS)"]).nsymbol, Nsymbol::Dbcs);
1218        assert_eq!(given(&["NSYMBOL(DBCS)", "NS(NAT)"]).nsymbol, Nsymbol::National);
1219        assert_eq!(given(&["DS(S)"]).dispsign, DispSign::Sep);
1220        assert_eq!(given(&["DISPSIGN(SEP)", "DISPSIGN(COMPAT)"]).dispsign, DispSign::Compat);
1221        assert_eq!(given(&["INTDATE(LILIAN)"]).intdate, IntDate::Lilian);
1222        assert_eq!(given(&["QUA(E)"]).qualify, Qualify::Extend);
1223        assert_eq!(given(&["QUALIFY(EXTEND)", "QUA(C)"]).qualify, Qualify::Compat);
1224        assert!(given(&["INITIAL"]).initial);
1225        assert!(!given(&["INITIAL", "NOINITIAL"]).initial);
1226        assert_eq!(given(&["VLR(C)"]).vlr, Vlr::Compat);
1227        assert_eq!(given(&["VLR(COMPAT)", "VLR(STANDARD)"]).vlr, Vlr::Standard);
1228        assert_eq!(given(&["VS(S)"]).vsamopenfs, VsamOpenFs::Succ);
1229        assert_eq!(given(&["VSAMOPENFS(SUCC)", "VSAMOPENFS(COMPAT)"]).vsamopenfs, VsamOpenFs::Compat);
1230        for bad in ["APOST(X)", "NSYMBOL(N)", "DS(X)", "INTDATE(JULIAN)", "QUA(X)", "INITIAL(Y)", "VLR(X)", "VS(X)", "INTDATE"] {
1231            assert!(matches!(Options::default().apply(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1232        }
1233    }
1234
1235    #[test]
1236    fn currency_takes_one_character_it_may_name_in_either_delimiter_or_a_hexadecimal_byte() {
1237        let currency = |card: &str| {
1238            let mut o = Options::default();
1239            o.apply(card).map(|_| o.currency)
1240        };
1241        assert_eq!(currency("CURRENCY('£')"), Ok(Some(Currency::Char('£'))));
1242        assert_eq!(currency("curr(\"f\")"), Ok(Some(Currency::Char('f'))), "the literal keeps its case");
1243        assert_eq!(currency("CURRENCY(X'5B')"), Ok(Some(Currency::Hex(0x5B))));
1244        assert_eq!(currency("NOCURR"), Ok(None));
1245        for bad in ["CURRENCY('E')", "CURRENCY('e')", "CURRENCY('1')", "CURRENCY(' ')", "CURRENCY('*')", "CURRENCY('EUR')", "CURRENCY(SPACE)", "CURRENCY(N'£')", "CURRENCY(Z'£')", "CURRENCY(X'5B5B')", "CURRENCY", "NOCURRENCY('£')"] {
1246            assert!(matches!(currency(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1247        }
1248        let mut o = Options::default();
1249        o.apply("CURRENCY(X'4A')").unwrap();
1250        assert_eq!(o.currency_symbol(), Some(Ok('¢')), "X'4A' is the cent sign in CCSID 1140");
1251        o.apply("CURRENCY(X'F1')").unwrap();
1252        assert_eq!(o.currency_symbol(), Some(Err('1')));
1253        assert_eq!(Options::default().currency_symbol(), None);
1254    }
1255
1256    #[test]
1257    fn numcheck_takes_ibms_suboption_defaults_and_zonecheck_is_its_zoned_check() {
1258        let numcheck = |card: &str| {
1259            let mut o = Options::default();
1260            o.apply(card).map(|_| o.numcheck)
1261        };
1262        let all = Numcheck::default();
1263        assert_eq!(all, Numcheck { zon: Some(ZonCheck { alphnum: true, lax: false }), pac: true, bin: Some(BinCheck { truncbin: true }), abd: false });
1264        assert_eq!(numcheck("NUMCHECK"), Ok(Some(all)));
1265        assert_eq!(numcheck("NC(ABD)"), Ok(Some(Numcheck { abd: true, ..all })));
1266        assert_eq!(numcheck("NUMCHECK(BIN)"), Ok(Some(Numcheck { zon: None, pac: false, bin: Some(BinCheck { truncbin: true }), abd: false })));
1267        assert_eq!(
1268            numcheck("NUMCHECK(ZON(NOALPHNUM,LAX),NOPAC,BIN(NOTRUNCBIN),ABD)"),
1269            Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: false, lax: true }), pac: false, bin: Some(BinCheck { truncbin: false }), abd: true }))
1270        );
1271        assert_eq!(numcheck("NUMCHECK(ZON(LAXREDEF))"), Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: true, lax: true }), pac: false, bin: None, abd: false })));
1272        assert_eq!(numcheck("NUMCHECK(NOZON,NOPAC,NOBIN)"), Ok(None));
1273        assert_eq!(numcheck("NONC"), Ok(None));
1274        for bad in ["NUMCHECK(ZON(X))", "NUMCHECK(PAC(X))", "NUMCHECK(BIN(X))", "NUMCHECK(X)", "ZONECHECK", "ZC(X)"] {
1275            assert!(matches!(numcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1276        }
1277        assert_eq!(numcheck("ZC(ABD)"), Ok(Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd: true })));
1278        let mut o = Options::default();
1279        o.apply("NUMCHECK").unwrap();
1280        o.apply("NOZONECHECK").unwrap();
1281        assert_eq!(o.numcheck, Some(Numcheck { zon: None, ..all }));
1282        o.apply("ZONECHECK(MSG)").unwrap();
1283        o.apply("NOZC").unwrap();
1284        assert_eq!(o.numcheck, None);
1285    }
1286
1287    #[test]
1288    fn parmcheck_and_initcheck_take_their_suboptions_and_ibms_defaults() {
1289        let parmcheck = |card: &str| {
1290            let mut o = Options::default();
1291            o.apply(card).map(|_| o.parmcheck)
1292        };
1293        assert_eq!(parmcheck("PARMCHECK"), Ok(Some(Parmcheck { abd: false, bytes: 100 })));
1294        assert_eq!(parmcheck("PC(ABD)"), Ok(Some(Parmcheck { abd: true, bytes: 100 })));
1295        assert_eq!(parmcheck("PC(5000)"), Ok(Some(Parmcheck { abd: false, bytes: 5000 })));
1296        assert_eq!(parmcheck("PARMCHECK(ABD,1)"), Ok(Some(Parmcheck { abd: true, bytes: 1 })));
1297        assert_eq!(parmcheck("NOPC"), Ok(None));
1298        for bad in ["PC(0)", "PC(10000)", "PC(5000,ABD)", "PC(MSG,1,2)", "PC(X)"] {
1299            assert!(matches!(parmcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1300        }
1301        let initcheck = |card: &str| {
1302            let mut o = Options::default();
1303            o.apply(card).map(|_| o.initcheck)
1304        };
1305        assert_eq!(initcheck("INITCHECK"), Ok(Some(Initcheck::Lax)));
1306        assert_eq!(initcheck("IC(STRICT)"), Ok(Some(Initcheck::Strict)));
1307        assert_eq!(initcheck("NOIC"), Ok(None));
1308        assert!(matches!(initcheck("IC(X)"), Err(OptionError::BadSuboption { .. })));
1309        let o = Options::default();
1310        assert_eq!((o.numcheck, o.parmcheck, o.initcheck), (None, None, None));
1311    }
1312
1313    #[test]
1314    fn cics_return_warning_is_once_unless_the_flag_says_always_or_never() {
1315        let mut o = Options::default();
1316        assert_eq!(o.cics_return_warning, CicsReturnWarning::Once);
1317        for mode in [CicsReturnWarning::Always, CicsReturnWarning::Never, CicsReturnWarning::Once] {
1318            o.apply_flag(mode.flag()).unwrap();
1319            assert_eq!(o.cics_return_warning, mode);
1320        }
1321        assert!(o.apply_flag("--cics-return-warning=sometimes").is_err());
1322        assert!(o.apply_flag("--cics-return-warning").is_err());
1323    }
1324
1325    #[test]
1326    fn compliance_is_strict_unless_the_flag_says_extended() {
1327        let mut o = Options::default();
1328        assert_eq!(o.compliance, Compliance::Strict);
1329        for level in [Compliance::Extended, Compliance::Strict] {
1330            o.apply_flag(level.flag()).unwrap();
1331            assert_eq!(o.compliance, level);
1332            assert_eq!(Compliance::named(level.name()), Some(level));
1333        }
1334        for bad in ["--compliance=EXTENDED", "--compliance=", "--compliance", "--compliance=mf"] {
1335            assert!(o.apply_flag(bad).is_err(), "{bad}");
1336        }
1337        let flags = |given: &[&str]| Compliance::of(&given.iter().map(|f| f.to_string()).collect::<Vec<_>>());
1338        assert_eq!(flags(&[]), Compliance::Strict);
1339        assert_eq!(flags(&["-silent", "--compliance=extended"]), Compliance::Extended);
1340        assert_eq!(flags(&["--compliance=extended", "--compliance=strict"]), Compliance::Strict);
1341    }
1342
1343    #[test]
1344    fn the_dialect_is_ibm_unless_the_flag_says_gnucobol() {
1345        let mut o = Options::default();
1346        assert_eq!(o.dialect, Dialect::Ibm);
1347        for dialect in [Dialect::Gnucobol, Dialect::Ibm] {
1348            o.apply_flag(dialect.flag()).unwrap();
1349            assert_eq!(o.dialect, dialect);
1350            assert_eq!(Dialect::named(dialect.name()), Some(dialect));
1351        }
1352        for bad in ["--dialect=GNUCOBOL", "--dialect=", "--dialect", "--dialect=mf"] {
1353            assert!(o.apply_flag(bad).is_err(), "{bad}");
1354        }
1355        assert_eq!(o.dialect, Dialect::Ibm);
1356    }
1357}