Skip to main content

ironwork_numeric/
options.rs

1use crate::assumptions;
2use std::fmt;
3use zarch::ebcdic::{self, CodePage};
4use zarch::hfp::Precision;
5
6/// Enterprise COBOL's compiler options, from Table 45 of IBM's Programming Guide, vendored byte for
7/// byte from cobolwork's provenance/enterprise-options.tsv (tools/sync-option-table.sh).
8const TABLE: &str = include_str!("../data/enterprise-options.tsv");
9
10/// An option in IBM's table: the spellings it answers to, and where it may be given.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct Documented {
13    pub name: &'static str,
14    spellings: &'static str,
15    /// Whether a CBL or PROCESS statement may give it.
16    pub process: bool,
17    /// Whether a CBL or PROCESS statement may give it only before a batch compilation's first program.
18    pub first_program_only: bool,
19    pub installation_default: bool,
20    /// The page of its section in the Enterprise COBOL 6.4 Programming Guide.
21    pub page: u16,
22}
23
24impl Documented {
25    pub fn spellings(&self) -> impl Iterator<Item = &'static str> {
26        self.spellings.split(' ')
27    }
28}
29
30/// Every option in IBM's table.
31pub fn documented() -> impl Iterator<Item = Documented> {
32    TABLE.lines().filter(|l| !l.starts_with('#')).map(|line| {
33        let f: Vec<&'static str> = line.split('\t').collect();
34        let yes = |i: usize| f[i] == "yes";
35        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") }
36    })
37}
38
39/// The option IBM documents under `spelling`, and whether the spelling turns the option off: a
40/// spelling beginning NO that is not the option's own name.
41pub fn spelled(spelling: &str) -> Option<(Documented, bool)> {
42    let o = documented().find(|o| o.spellings().any(|s| s == spelling))?;
43    Some((o, spelling.starts_with("NO") && !o.name.split('/').any(|n| n == spelling)))
44}
45
46/// Whether `option`, as a CBL or PROCESS statement writes it, sets the on-off option `name`, and to
47/// which: `SSR(ZLEN)` gives Some(true) for SSRANGE, `NOSSR` Some(false), and TRUNC(OPT) None.
48pub fn switch(option: &str, name: &str) -> Option<bool> {
49    let option = option.trim().to_ascii_uppercase();
50    let word = option.split('(').next().unwrap_or("").trim();
51    spelled(word).filter(|(o, _)| o.name == name).map(|(_, off)| !off)
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum Arith {
56    #[default]
57    Compat,
58    Extend,
59}
60
61impl Arith {
62    pub const fn max_picture_digits(self) -> u32 {
63        match self {
64            Self::Compat => 18,
65            Self::Extend => 31,
66        }
67    }
68
69    pub const fn intermediate_digits(self) -> u32 {
70        match self {
71            Self::Compat => 30,
72            Self::Extend => 31,
73        }
74    }
75
76    pub const fn float_intermediate(self) -> Precision {
77        match self {
78            Self::Compat => Precision::Long,
79            Self::Extend => Precision::Extended,
80        }
81    }
82}
83
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
85pub enum Trunc {
86    #[default]
87    Std,
88    Opt,
89    Bin,
90}
91
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub enum Numproc {
94    #[default]
95    Nopfd,
96    Pfd,
97}
98
99/// Whether checked mode reports: a store that TRUNC(OPT) leaves to the generated code, when decimal
100/// and binary truncation disagree, and a SORT whose outcome FASTSRT changes. `-silent` turns the
101/// reports off.
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
103pub enum TruncCheck {
104    #[default]
105    Report,
106    Silent,
107}
108
109/// How SORT and MERGE compare a zoned or packed key: as DFSORT compares ZD and PD fields, so no
110/// bytes are invalid, or (`-strict-sort-keys`) as the program reads the item, so an invalid one is a
111/// data exception.
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub enum SortKeys {
114    #[default]
115    Dfsort,
116    Strict,
117}
118
119/// Whether FASTSRT gives DFSORT the I/O of a USING or GIVING print file under ADV, whose data set's
120/// records are a byte longer than its FD's: never (`--fastsrt-adv-print=exclude`), or as any other
121/// file's (`--fastsrt-adv-print=include`).
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
123pub enum FastsrtAdvPrint {
124    #[default]
125    Exclude,
126    Include,
127}
128
129impl FastsrtAdvPrint {
130    pub const fn flag(self) -> &'static str {
131        match self {
132            Self::Exclude => "--fastsrt-adv-print=exclude",
133            Self::Include => "--fastsrt-adv-print=include",
134        }
135    }
136}
137
138/// INVDATA: zoned and packed items may hold invalid digits, sign codes or zone bits. FORCENUMCMP
139/// compares zoned items as numbers whatever their zones; CLEANSIGN cleans a sign code on input to
140/// a comparison or computation (Programming Guide SC27-8714-03, pp. 376-378). ZONEDATA(NOPFD) and
141/// ZONEDATA(MIG) are INVDATA with FORCENUMCMP off and on.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct Invdata {
144    pub forcenumcmp: bool,
145    pub cleansign: bool,
146}
147
148impl Default for Invdata {
149    fn default() -> Self {
150        Self { forcenumcmp: false, cleansign: true }
151    }
152}
153
154/// NUMCHECK: implicit numeric class tests of zoned and packed senders, and size tests of binary
155/// senders, each a warning that lets the statement run (MSG) or a terminating message (`abd`)
156/// (Programming Guide SC27-8714-03, pp. 388-392). ZONECHECK(MSG|ABD) is NUMCHECK(ZON,MSG|ABD)
157/// (p. 427).
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct Numcheck {
160    pub zon: Option<ZonCheck>,
161    pub pac: bool,
162    pub bin: Option<BinCheck>,
163    pub abd: bool,
164}
165
166impl Default for Numcheck {
167    fn default() -> Self {
168        Self { zon: Some(ZonCheck::default()), pac: true, bin: Some(BinCheck { truncbin: true }), abd: false }
169    }
170}
171
172/// ZON's suboptions: whether a zoned item compared with an alphanumeric operand is checked
173/// (ALPHNUM, the default), and whether the redefinitions p. 390 lists are tolerated (LAX).
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub struct ZonCheck {
176    pub alphnum: bool,
177    pub lax: bool,
178}
179
180impl Default for ZonCheck {
181    fn default() -> Self {
182        Self { alphnum: true, lax: false }
183    }
184}
185
186/// BIN's suboption: whether binary senders are checked under TRUNC(BIN) too (TRUNCBIN, the default).
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub struct BinCheck {
189    pub truncbin: bool,
190}
191
192/// PARMCHECK: a buffer of `bytes` after the last WORKING-STORAGE item, set to X'AA' before each
193/// CALL and checked after it, a change being a warning (MSG) or a terminating message (`abd`)
194/// (Programming Guide SC27-8714-03, p. 397).
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct Parmcheck {
197    pub abd: bool,
198    pub bytes: u16,
199}
200
201/// INITCHECK: a compile-time warning for a WORKING-STORAGE or LOCAL-STORAGE item used before it is
202/// set on some path (`Lax`, the default) or on any path (`Strict`) (Programming Guide
203/// SC27-8714-03, pp. 373-374).
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
205pub enum Initcheck {
206    #[default]
207    Lax,
208    Strict,
209}
210
211/// What a program with no STOP RUN, GOBACK or EXIT PROGRAM that ends with EXEC CICS RETURN or XCTL
212/// gets (assumption C124): IBM's warning (`--cics-return-warning=always`), one informational note
213/// in place of it (`once`), or nothing (`never`).
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
215pub enum CicsReturnWarning {
216    #[default]
217    Once,
218    Always,
219    Never,
220}
221
222impl CicsReturnWarning {
223    pub const fn flag(self) -> &'static str {
224        match self {
225            Self::Once => "--cics-return-warning=once",
226            Self::Always => "--cics-return-warning=always",
227            Self::Never => "--cics-return-warning=never",
228        }
229    }
230}
231
232/// What the figurative constant QUOTE is: a quotation mark under QUOTE, IBM's default, or an
233/// apostrophe under APOST (Programming Guide SC27-8714-03, p. 347).
234#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
235pub enum Quote {
236    #[default]
237    Quote,
238    Apost,
239}
240
241impl Quote {
242    /// The character in EBCDIC, where every single-byte page carries it at the same place.
243    pub const fn byte(self) -> u8 {
244        match self {
245            Self::Quote => ebcdic::QUOTE,
246            Self::Apost => ebcdic::APOSTROPHE,
247        }
248    }
249
250    /// The character as a UTF-16 unit, for a national item.
251    pub const fn unit(self) -> u16 {
252        match self {
253            Self::Quote => 0x0022,
254            Self::Apost => 0x0027,
255        }
256    }
257}
258
259/// CURRENCY(literal): the character, or a hexadecimal literal's byte, which the program's code page
260/// turns into one (Programming Guide SC27-8714-03, p. 358).
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum Currency {
263    Char(char),
264    Hex(u8),
265}
266
267/// Whether N literals, and PICTURE N items with no USAGE clause, are national, IBM's default, or
268/// DBCS (Programming Guide SC27-8714-03, pp. 387-388).
269#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
270pub enum Nsymbol {
271    #[default]
272    National,
273    Dbcs,
274}
275
276/// How DISPLAY shows a signed binary, packed or overpunched zoned item: as releases before 6 did,
277/// with an overpunched digit (`Compat`), or with a separate leading sign (`Sep`) (Programming
278/// Guide SC27-8714-03, pp. 362-363).
279#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
280pub enum DispSign {
281    #[default]
282    Compat,
283    Sep,
284}
285
286/// Day 1 of the date intrinsic functions' integer dates: 1 January 1601 (`Ansi`), or Language
287/// Environment's Lilian 15 October 1582 (Programming Guide SC27-8714-03, p. 375).
288#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
289pub enum IntDate {
290    #[default]
291    Ansi,
292    Lilian,
293}
294
295/// How an external program-name or user-defined function-name is formed (Programming Guide
296/// SC27-8714-03, pp. 398-399, PGMNAME).
297#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
298pub enum Pgmname {
299    /// Folded to upper case, truncated to eight characters, hyphens made 0, a leading digit made a
300    /// letter.
301    #[default]
302    Compat,
303    /// As `Compat`, without the truncation.
304    LongUpper,
305    /// As written.
306    LongMixed,
307}
308
309impl Pgmname {
310    pub fn named(sub: &str) -> Option<Self> {
311        match sub {
312            "COMPAT" | "CO" => Some(Self::Compat),
313            "LONGUPPER" | "UPPER" | "LU" | "U" => Some(Self::LongUpper),
314            "LONGMIXED" | "MIXED" | "LM" | "M" => Some(Self::LongMixed),
315            _ => None,
316        }
317    }
318
319    /// The setting the last valid PGMNAME among the option cards gives. It forms names as the
320    /// source is read, so the compiled program does not carry it.
321    pub fn of(options: &[String]) -> Self {
322        let named = |o: &String| {
323            let o = o.trim().to_ascii_uppercase();
324            let (name, sub) = o.split_once('(')?;
325            spelled(name.trim()).filter(|(d, _)| d.name == "PGMNAME")?;
326            Self::named(sub.trim_end_matches(')').trim())
327        };
328        options.iter().rev().find_map(named).unwrap_or_default()
329    }
330
331    /// The external name `name` becomes under this setting.
332    pub fn external(self, name: &str) -> String {
333        if self == Self::LongMixed {
334            return name.to_owned();
335        }
336        let kept = if self == Self::Compat { name.chars().take(8).collect::<String>() } else { name.to_owned() };
337        let mut out: String = kept.chars().map(|c| if c == '-' { '0' } else { c.to_ascii_uppercase() }).collect();
338        if let Some(first) = out.chars().next().filter(|c| !c.is_ascii_alphabetic() && *c != '_') {
339            let letter = match first {
340                '1'..='9' => char::from(b'A' + (first as u8 - b'1')),
341                _ => 'J',
342            };
343            out.replace_range(..first.len_utf8(), &letter.to_string());
344        }
345        out
346    }
347}
348
349/// Whether a reference must be unique by the standard's rules (`Compat`), or resolves to the one
350/// item a complete set of qualifiers names (`Extend`) (Programming Guide SC27-8714-03, p. 400).
351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
352pub enum Qualify {
353    #[default]
354    Compat,
355    Extend,
356}
357
358/// What a READ of a variable-length record checks its length against: the level-01 records under
359/// `Standard`, RECORD VARYING under `Compat`; outside them its status is 04, else 00 (Programming
360/// Guide SC27-8714-03, pp. 422-424, Table 52). See assumptions C218 and C219.
361#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
362pub enum Vlr {
363    #[default]
364    Standard,
365    Compat,
366}
367
368/// The file status of a VSAM OPEN that succeeds once its file's integrity is verified: 97 under
369/// `Compat`, 00 under `Succ` (Programming Guide SC27-8714-03, p. 424). OPEN verifies a data set a
370/// run left open for output (assumption C220).
371#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
372pub enum VsamOpenFs {
373    #[default]
374    Compat,
375    Succ,
376}
377
378/// Whether a program whose compile gave warnings runs (`Proceed`), or (`-warnings-block`, the
379/// command line's NOCOMPILE(W)) is refused. The return code is 4 either way.
380#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
381pub enum Warnings {
382    #[default]
383    Proceed,
384    Block,
385}
386
387/// Whose result a computation gives where ironwork knowingly differs from GnuCOBOL: Enterprise
388/// COBOL's as the assumptions register reads it (`Ibm`), or that of GnuCOBOL's
389/// `cobc -std=ibm-strict` (`Gnucobol`, `--dialect gnucobol`), so a migration can compare ironwork
390/// with a GnuCOBOL build. docs/dialect.md lists each difference.
391#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
392pub enum Dialect {
393    #[default]
394    Ibm,
395    Gnucobol,
396}
397
398impl Dialect {
399    pub const fn flag(self) -> &'static str {
400        match self {
401            Self::Ibm => "--dialect=ibm",
402            Self::Gnucobol => "--dialect=gnucobol",
403        }
404    }
405
406    /// The value `--dialect` takes for it.
407    pub const fn name(self) -> &'static str {
408        match self {
409            Self::Ibm => "ibm",
410            Self::Gnucobol => "gnucobol",
411        }
412    }
413
414    pub fn named(value: &str) -> Option<Self> {
415        [Self::Ibm, Self::Gnucobol].into_iter().find(|d| d.name() == value)
416    }
417}
418
419/// A chosen assumption `--assume ID=VALUE` switches, by its place in [`SWITCHES`].
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421pub enum Switched {
422    RoundedExtraPlace,
423    DisplayOfNondisplayNumeric,
424    DecimalCommaDisplayLiteral,
425    AcceptAtEnd,
426    EntryCalls,
427    ExternalStorage,
428    OptimizedZonesCompared,
429}
430
431const IBM_OR_GNUCOBOL: &[&str] = &["ibm", "gnucobol"];
432
433/// Each switched assumption's register id and the values `--assume` takes for it: `ibm`, the result
434/// the register states, then `gnucobol`, cobc's, which `--dialect gnucobol` gives, then any other
435/// alternative the claim names (docs/dialect.md).
436pub const SWITCHES: [(&str, &[&str]); 7] = [
437    (assumptions::ROUNDED_EXTRA_PLACE, &["ibm", "gnucobol", "off"]),
438    (assumptions::DISPLAY_OF_NONDISPLAY_NUMERIC, IBM_OR_GNUCOBOL),
439    (assumptions::DECIMAL_COMMA_DISPLAY_LITERAL, IBM_OR_GNUCOBOL),
440    (assumptions::ACCEPT_AT_END, IBM_OR_GNUCOBOL),
441    (assumptions::ENTRY_CALLS, IBM_OR_GNUCOBOL),
442    (assumptions::EXTERNAL_STORAGE, IBM_OR_GNUCOBOL),
443    (assumptions::OPTIMIZED_ZONES_COMPARED, IBM_OR_GNUCOBOL),
444];
445
446/// Where a ROUNDED receiver's extra decimal place counts (assumption C101): in every operation of
447/// the statement (`ibm`), in its last alone (`gnucobol`), or in none (`off`).
448#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449pub enum ExtraPlace {
450    Every,
451    Last,
452    Off,
453}
454
455/// The values `--assume` gave, by place in [`SWITCHES`]: 0 where none did and the dialect decides,
456/// else one more than the value's place in the switch's list.
457#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
458pub struct Assumed {
459    pub given: [u8; SWITCHES.len()],
460}
461
462impl Assumed {
463    /// Reads `ID=VALUE` as a switch's place and the value's mark, refusing by name an id the
464    /// register lacks, an assumption with no alternative, and a value the switch does not take.
465    pub fn parse(spec: &str) -> Result<(usize, u8), String> {
466        let Some((id, value)) = spec.split_once('=') else {
467            return Err(format!("--assume {spec}: needs ID=VALUE, such as C101=off"));
468        };
469        let Some(i) = SWITCHES.iter().position(|(s, _)| *s == id) else {
470            let switched = listed(&SWITCHES.map(|(s, _)| s), "and");
471            return Err(match assumptions::ASSUMPTIONS.iter().find(|a| a.id == id) {
472                Some(a) => format!("--assume {spec}: assumption {id} ({}) has no alternative; --assume switches {switched}", a.basis.name()),
473                None => format!("--assume {spec}: the register has no assumption {id}; ironwork assumptions lists them"),
474            });
475        };
476        match SWITCHES[i].1.iter().position(|v| *v == value) {
477            Some(k) => Ok((i, k as u8 + 1)),
478            None => Err(format!("--assume {spec}: {id} takes {}", listed(SWITCHES[i].1, "or"))),
479        }
480    }
481}
482
483/// `a, b and c`, or with `or`.
484fn listed(words: &[&str], last: &str) -> String {
485    match words {
486        [] => String::new(),
487        [one] => (*one).to_owned(),
488        [rest @ .., end] => format!("{} {last} {end}", rest.join(", ")),
489    }
490}
491
492/// The COMPILE option: which messages stop the object code, so that run and cics refuse the
493/// program (Programming Guide SC27-8714-03, p. 355).
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495pub enum Compile {
496    /// COMPILE: object code whatever the messages, unless one is U and the compilation ended.
497    Full,
498    /// NOCOMPILE(W), NOCOMPILE(E) or NOCOMPILE(S): none from the first message of that severity up.
499    Until(Stop),
500    /// NOCOMPILE: a syntax check, with no object code at all.
501    SyntaxOnly,
502}
503
504/// A severity NOCOMPILE names.
505#[derive(Clone, Copy, Debug, PartialEq, Eq)]
506pub enum Stop {
507    W,
508    E,
509    S,
510}
511
512impl Default for Compile {
513    fn default() -> Self {
514        Self::Until(Stop::S)
515    }
516}
517
518impl Compile {
519    /// The lowest return code (Programming Guide Table 38, p. 282) of a message that stops the object
520    /// code: 0 stops it whatever the messages.
521    pub const fn stops_at(self) -> u8 {
522        match self {
523            Self::Full => 16,
524            Self::Until(Stop::S) => 12,
525            Self::Until(Stop::E) => 8,
526            Self::Until(Stop::W) => 4,
527            Self::SyntaxOnly => 0,
528        }
529    }
530}
531
532/// Whether the compile refuses what Enterprise COBOL refuses (`Strict`), or accepts, each with a
533/// warning, the other dialects' extensions docs/compliance.md lists (`Extended`,
534/// `--compliance extended`).
535#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
536pub enum Compliance {
537    #[default]
538    Strict,
539    Extended,
540}
541
542impl Compliance {
543    pub const fn flag(self) -> &'static str {
544        match self {
545            Self::Strict => "--compliance=strict",
546            Self::Extended => "--compliance=extended",
547        }
548    }
549
550    /// The value `--compliance` takes for it.
551    pub const fn name(self) -> &'static str {
552        match self {
553            Self::Strict => "strict",
554            Self::Extended => "extended",
555        }
556    }
557
558    pub fn named(value: &str) -> Option<Self> {
559        [Self::Strict, Self::Extended].into_iter().find(|c| c.name() == value)
560    }
561
562    /// The level the last `--compliance=` flag among `flags` gives, strict without one.
563    pub fn of(flags: &[String]) -> Self {
564        flags.iter().rev().find_map(|f| f.strip_prefix("--compliance=").and_then(Self::named)).unwrap_or_default()
565    }
566}
567
568/// Which contained programs a CALL reaches: Enterprise COBOL's scope rules (`Strict`, Language
569/// Reference, Conventions for program-names), or any program of the run unit by its name
570/// (`Flexible`, `--program-scope=flexible`).
571#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
572pub enum ProgramScope {
573    #[default]
574    Strict,
575    Flexible,
576}
577
578/// What a static CALL of a name no program of its compilation has does: compiles as a CALL found
579/// when it runs (`Run`), or is refused when compiled (`Fail`, `--unresolved-calls=fail`), as
580/// IBM's binder refuses a reference it cannot resolve.
581#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
582pub enum UnresolvedCalls {
583    #[default]
584    Run,
585    Fail,
586}
587
588/// Whether a CALL of a Language Environment service's name finds a program of that name first
589/// (`Programs`, assumption L1), or always the service (`Bind`, `--le-services=bind`).
590#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
591pub enum LeServices {
592    #[default]
593    Programs,
594    Bind,
595}
596
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub struct Options {
599    pub arith: Arith,
600    pub trunc: Trunc,
601    pub numproc: Numproc,
602    pub codepage: u16,
603    pub trunc_check: TruncCheck,
604    /// FASTSRT: DFSORT does the I/O of a SORT's USING and GIVING files where IBM's rules allow.
605    pub fastsrt: bool,
606    pub fastsrt_adv_print: FastsrtAdvPrint,
607    pub sort_keys: SortKeys,
608    /// ADV: a print file's printer control character is a byte added before each record; under
609    /// NOADV it is the record's own first byte.
610    pub adv: bool,
611    /// THREAD, DLL, RENT and DBCS, which object-oriented programs are compiled with.
612    pub thread: bool,
613    pub dll: bool,
614    pub rent: bool,
615    pub dbcs: bool,
616    pub warnings: Warnings,
617    /// COMPILE or NOCOMPILE as a CBL or PROCESS card gave it; None when none did.
618    pub compile: Option<Compile>,
619    /// DYNAM: a CALL of a literal loads the program at run time, as a CALL of an identifier does.
620    pub dynam: bool,
621    /// The Language Environment runtime option DEBUG (`-debug`): USE FOR DEBUGGING procedures run.
622    /// NODEBUG, IBM's default, keeps them from running (assumption C63).
623    pub debug: bool,
624    pub cics_return_warning: CicsReturnWarning,
625    /// INVDATA's suboptions; None for NOINVDATA, IBM's default, which assumes the data is valid.
626    pub invdata: Option<Invdata>,
627    /// ZWB: a signed zoned item compared with a nonnumeric operand loses its sign first.
628    pub zwb: bool,
629    pub quote: Quote,
630    /// CURRENCY(literal): the currency symbol a program with no CURRENCY SIGN clause uses in place
631    /// of $. None under NOCURRENCY, IBM's default.
632    pub currency: Option<Currency>,
633    pub nsymbol: Nsymbol,
634    pub dispsign: DispSign,
635    pub intdate: IntDate,
636    pub qualify: Qualify,
637    /// INITIAL: the program and its nested programs behave as though their PROGRAM-ID paragraphs
638    /// said IS INITIAL (Programming Guide SC27-8714-03, p. 374). A compile with THREAD takes
639    /// NOINITIAL (p. 344).
640    pub initial: bool,
641    pub vlr: Vlr,
642    pub vsamopenfs: VsamOpenFs,
643    pub numcheck: Option<Numcheck>,
644    pub parmcheck: Option<Parmcheck>,
645    pub initcheck: Option<Initcheck>,
646    /// OPTIMIZE's level, 0 to 2.
647    pub optimize: u8,
648    pub compliance: Compliance,
649    pub dialect: Dialect,
650    pub program_scope: ProgramScope,
651    pub unresolved_calls: UnresolvedCalls,
652    pub le_services: LeServices,
653    pub assumed: Assumed,
654}
655
656impl Default for Options {
657    fn default() -> Self {
658        Self {
659            arith: Arith::default(),
660            trunc: Trunc::default(),
661            numproc: Numproc::default(),
662            codepage: 1140,
663            trunc_check: TruncCheck::default(),
664            fastsrt: false,
665            fastsrt_adv_print: FastsrtAdvPrint::default(),
666            sort_keys: SortKeys::default(),
667            adv: true,
668            thread: false,
669            dll: false,
670            rent: true,
671            dbcs: true,
672            warnings: Warnings::default(),
673            compile: None,
674            dynam: false,
675            debug: false,
676            cics_return_warning: CicsReturnWarning::default(),
677            invdata: None,
678            zwb: true,
679            quote: Quote::default(),
680            currency: None,
681            nsymbol: Nsymbol::default(),
682            dispsign: DispSign::default(),
683            intdate: IntDate::default(),
684            qualify: Qualify::default(),
685            initial: false,
686            vlr: Vlr::default(),
687            vsamopenfs: VsamOpenFs::default(),
688            numcheck: None,
689            parmcheck: None,
690            initcheck: None,
691            optimize: 0,
692            compliance: Compliance::default(),
693            dialect: Dialect::default(),
694            program_scope: ProgramScope::default(),
695            unresolved_calls: UnresolvedCalls::default(),
696            le_services: LeServices::default(),
697            assumed: Assumed::default(),
698        }
699    }
700}
701
702/// What an option could not do. Each but UnsupportedCodePage and UnknownFlag is a message IBM's
703/// compiler gives and carries on from (assumptions C120 to C122).
704#[derive(Clone, Debug, PartialEq, Eq)]
705pub enum OptionError {
706    /// A suboption the option does not have: the option is discarded.
707    BadSuboption { option: String, given: String },
708    /// A suboption IBM removed, replaced by `instead`.
709    Removed { option: String, since: &'static str, instead: &'static str },
710    /// An option Enterprise COBOL 6.4 does not have, given a warning or (`warning` false) an
711    /// informational message and no effect.
712    NoEffect { option: &'static str, why: &'static str, warning: bool },
713    UnsupportedCodePage(u16),
714    UnknownFlag(String),
715}
716
717impl fmt::Display for OptionError {
718    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
719        match self {
720            Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
721            Self::Removed { option, since, instead } => write!(f, "{option} was removed in Enterprise COBOL {since}, so {instead} is in effect"),
722            Self::NoEffect { option, why, .. } => write!(f, "{option} {why}"),
723            Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not an EBCDIC page this compiler carries"),
724            Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
725        }
726    }
727}
728
729/// The options IBM removed from Enterprise COBOL that a 6.4 compile accepts without effect, by
730/// spelling: LIB and SIZE (Migration Guide GC27-8715-03, Table 32, p. 167), FLAGSAA and NOFDUMP
731/// (Table 23, p. 112).
732fn without_effect(name: &str) -> Option<OptionError> {
733    let (option, why, warning) = match name {
734        "LIB" => ("LIB", "is no longer needed: COPY members are always read from the libraries", false),
735        "SIZE" | "SZ" => ("SIZE", "was removed in Enterprise COBOL V5 and has no effect", false),
736        "FLAGSAA" => ("FLAGSAA", "is not an Enterprise COBOL option and has no effect", true),
737        "NOFDUMP" => ("NOFDUMP", "is not an Enterprise COBOL option and has no effect", true),
738        _ => return None,
739    };
740    Some(OptionError::NoEffect { option, why, warning })
741}
742
743/// CURRENCY's literal: one character between quotation marks or apostrophes, or a hexadecimal
744/// literal of one byte (Programming Guide SC27-8714-03, p. 358). A figurative constant, a
745/// null-terminated, DBCS or national literal is none of these, so it is refused here.
746fn currency_literal(text: &str) -> Option<Currency> {
747    fn quoted(t: &str) -> Option<&str> {
748        let q = t.chars().next().filter(|q| matches!(q, '\'' | '"'))?;
749        (t.len() >= 2 && t.ends_with(q)).then(|| &t[1..t.len() - 1])
750    }
751    let text = text.trim();
752    if let Some(hex) = text.strip_prefix(['X', 'x']).and_then(quoted) {
753        return u8::from_str_radix(hex, 16).ok().filter(|_| hex.len() == 2).map(Currency::Hex);
754    }
755    let mut chars = quoted(text)?.chars();
756    match (chars.next(), chars.next()) {
757        (Some(c), None) if can_be_currency(c) => Some(Currency::Char(c)),
758        _ => None,
759    }
760}
761
762/// The text between an option's first opening parenthesis and its last closing one.
763fn inner(option: &str) -> &str {
764    option.split_once('(').map_or("", |(_, rest)| rest.trim_end().strip_suffix(')').unwrap_or(rest)).trim()
765}
766
767/// `text` split at the commas outside parentheses.
768fn top_level(text: &str) -> Vec<&str> {
769    let (mut parts, mut depth, mut start) = (Vec::new(), 0i32, 0);
770    for (i, c) in text.char_indices() {
771        match c {
772            '(' => depth += 1,
773            ')' => depth -= 1,
774            ',' if depth == 0 => {
775                parts.push(text[start..i].trim());
776                start = i + 1;
777            }
778            _ => {}
779        }
780    }
781    parts.push(text[start..].trim());
782    parts.into_iter().filter(|p| !p.is_empty()).collect()
783}
784
785/// NUMCHECK's suboptions with IBM's defaults: none gives every data type and MSG; data types left
786/// out are on when none is given and off when any is; every data type off is NONUMCHECK, None
787/// (Programming Guide SC27-8714-03, p. 389). None inside Some is a suboption IBM does not have.
788fn numcheck(text: &str) -> Option<Option<Numcheck>> {
789    let (mut zon, mut pac, mut bin, mut abd, mut typed) = (None, None, None, false, false);
790    for part in top_level(text) {
791        let (word, args) = match part.split_once('(') {
792            Some((w, _)) => (w.trim(), top_level(inner(part))),
793            None => (part, Vec::new()),
794        };
795        match word {
796            "ZON" => {
797                let mut z = ZonCheck::default();
798                for a in args {
799                    match a {
800                        "ALPHNUM" => z.alphnum = true,
801                        "NOALPHNUM" => z.alphnum = false,
802                        "LAX" | "LAXREDEF" => z.lax = true,
803                        "STRICT" | "STRICTREDEF" => z.lax = false,
804                        _ => return None,
805                    }
806                }
807                zon = Some(Some(z));
808            }
809            "NOZON" if args.is_empty() => zon = Some(None),
810            "PAC" if args.is_empty() => pac = Some(true),
811            "NOPAC" if args.is_empty() => pac = Some(false),
812            "BIN" => {
813                bin = Some(Some(BinCheck {
814                    truncbin: match args[..] {
815                        [] | ["TRUNCBIN"] => true,
816                        ["NOTRUNCBIN"] => false,
817                        _ => return None,
818                    },
819                }))
820            }
821            "NOBIN" if args.is_empty() => bin = Some(None),
822            "MSG" if args.is_empty() => abd = false,
823            "ABD" if args.is_empty() => abd = true,
824            _ => return None,
825        }
826        typed |= matches!(word, "ZON" | "NOZON" | "PAC" | "NOPAC" | "BIN" | "NOBIN");
827    }
828    let all = Numcheck::default();
829    let n = if typed {
830        Numcheck { zon: zon.flatten(), pac: pac.unwrap_or(false), bin: bin.flatten(), abd }
831    } else {
832        Numcheck { abd, ..all }
833    };
834    Some((n.zon.is_some() || n.pac || n.bin.is_some()).then_some(n))
835}
836
837/// PARMCHECK's MSG or ABD and buffer size, 100 bytes and MSG when left out (Programming Guide
838/// SC27-8714-03, p. 397).
839fn parmcheck(sub: &str) -> Option<Parmcheck> {
840    let mut p = Parmcheck { abd: false, bytes: 100 };
841    let parts: Vec<&str> = sub.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
842    if parts.len() > 2 {
843        return None;
844    }
845    for (i, part) in parts.iter().enumerate() {
846        match *part {
847            "MSG" if i == 0 => p.abd = false,
848            "ABD" if i == 0 => p.abd = true,
849            n => p.bytes = n.parse().ok().filter(|b| (1..=9999).contains(b))?,
850        }
851    }
852    Some(p)
853}
854
855/// Whether the CURRENCY option may name `c`: a single-byte character that is no digit, space, one
856/// of the letters A B C D E G N P R S U V X Z in either case, or one of * + - / , . ; ( ) " =.
857pub fn can_be_currency(c: char) -> bool {
858    u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz *+-/,.;()\"=".contains(c)
859}
860
861impl std::error::Error for OptionError {}
862
863impl Options {
864    /// Applies one IBM compiler option as a CBL or PROCESS card or PARM writes it, e.g.
865    /// `TRUNC(OPT)`, `AR(E)`, `CP(1047)`. Returns false for an option this layer does not read. An
866    /// error leaves the options as they were, except NUMPROC(MIG), which sets the default NUMPROC.
867    pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
868        let given = option.trim();
869        let option = given.to_ascii_uppercase();
870        let (name, sub) = match option.split_once('(') {
871            Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
872            None => (option.as_str(), ""),
873        };
874        let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
875        if let Some(e) = without_effect(name) {
876            return Err(e);
877        }
878        let Some((documented, off)) = spelled(name) else { return Ok(false) };
879        match documented.name {
880            "ARITH" => {
881                self.arith = match sub {
882                    "COMPAT" | "C" => Arith::Compat,
883                    "EXTEND" | "E" => Arith::Extend,
884                    _ => return Err(bad()),
885                }
886            }
887            "TRUNC" => {
888                self.trunc = match sub {
889                    "STD" => Trunc::Std,
890                    "OPT" => Trunc::Opt,
891                    "BIN" => Trunc::Bin,
892                    _ => return Err(bad()),
893                }
894            }
895            "NUMPROC" => {
896                self.numproc = match sub {
897                    "NOPFD" => Numproc::Nopfd,
898                    "PFD" => Numproc::Pfd,
899                    "MIG" => {
900                        self.numproc = Numproc::default();
901                        return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5", instead: "NUMPROC(NOPFD)" });
902                    }
903                    _ => return Err(bad()),
904                }
905            }
906            "CODEPAGE" => {
907                let ccsid: u16 = sub.parse().map_err(|_| bad())?;
908                CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
909                self.codepage = ccsid;
910            }
911            "COMPILE" => {
912                self.compile = Some(match (off, sub) {
913                    (false, "") => Compile::Full,
914                    (true, "") => Compile::SyntaxOnly,
915                    (true, "W") => Compile::Until(Stop::W),
916                    (true, "E") => Compile::Until(Stop::E),
917                    (true, "S") => Compile::Until(Stop::S),
918                    _ => return Err(bad()),
919                })
920            }
921            "INVDATA" if off => self.invdata = None,
922            "INVDATA" => {
923                let mut invdata = Invdata::default();
924                for part in sub.split(',').map(str::trim).filter(|p| !p.is_empty()) {
925                    match part {
926                        "FORCENUMCMP" | "FNC" => invdata.forcenumcmp = true,
927                        "NOFORCENUMCMP" | "NOFNC" => invdata.forcenumcmp = false,
928                        "CLEANSIGN" | "CS" => invdata.cleansign = true,
929                        "NOCLEANSIGN" | "NOCS" => invdata.cleansign = false,
930                        _ => return Err(bad()),
931                    }
932                }
933                self.invdata = Some(invdata);
934            }
935            "ZONEDATA" => {
936                self.invdata = match sub {
937                    "PFD" => None,
938                    "NOPFD" => Some(Invdata::default()),
939                    "MIG" => Some(Invdata { forcenumcmp: true, cleansign: true }),
940                    _ => return Err(bad()),
941                }
942            }
943            "ZWB" => self.zwb = !off,
944            "NUMCHECK" if off => self.numcheck = None,
945            "NUMCHECK" => self.numcheck = numcheck(inner(&option)).ok_or_else(bad)?,
946            "ZONECHECK" if off => self.numcheck = self.numcheck.map(|n| Numcheck { zon: None, ..n }).filter(|n| n.pac || n.bin.is_some()),
947            "ZONECHECK" => {
948                let abd = match sub {
949                    "MSG" => false,
950                    "ABD" => true,
951                    _ => return Err(bad()),
952                };
953                self.numcheck = Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd });
954            }
955            "PARMCHECK" if off => self.parmcheck = None,
956            "PARMCHECK" => self.parmcheck = Some(parmcheck(sub).ok_or_else(bad)?),
957            "INITCHECK" if off => self.initcheck = None,
958            "INITCHECK" => {
959                self.initcheck = Some(match sub {
960                    "" | "LAX" => Initcheck::Lax,
961                    "STRICT" => Initcheck::Strict,
962                    _ => return Err(bad()),
963                })
964            }
965            "FASTSRT" => self.fastsrt = !off,
966            "ADV" => self.adv = !off,
967            "THREAD" => self.thread = !off,
968            "DLL" => self.dll = !off,
969            "RENT" => self.rent = !off,
970            "DBCS" => self.dbcs = !off,
971            "DYNAM" => self.dynam = !off,
972            "APOST/QUOTE" if sub.is_empty() => self.quote = if name == "APOST" { Quote::Apost } else { Quote::Quote },
973            "CURRENCY" => {
974                self.currency = match (off, sub) {
975                    (true, "") => None,
976                    (false, _) => Some(currency_literal(given.split_once('(').map_or("", |(_, rest)| rest.trim_end().trim_end_matches(')'))).ok_or_else(bad)?),
977                    _ => return Err(bad()),
978                }
979            }
980            "NSYMBOL" => {
981                self.nsymbol = match sub {
982                    "NATIONAL" | "NAT" => Nsymbol::National,
983                    "DBCS" => Nsymbol::Dbcs,
984                    _ => return Err(bad()),
985                }
986            }
987            "DISPSIGN" => {
988                self.dispsign = match sub {
989                    "COMPAT" | "C" => DispSign::Compat,
990                    "SEP" | "S" => DispSign::Sep,
991                    _ => return Err(bad()),
992                }
993            }
994            "INTDATE" => {
995                self.intdate = match sub {
996                    "ANSI" => IntDate::Ansi,
997                    "LILIAN" => IntDate::Lilian,
998                    _ => return Err(bad()),
999                }
1000            }
1001            "PGMNAME" if Pgmname::named(sub).is_some() => {}
1002            "PGMNAME" => return Err(bad()),
1003            "QUALIFY" => {
1004                self.qualify = match sub {
1005                    "COMPAT" | "C" => Qualify::Compat,
1006                    "EXTEND" | "E" => Qualify::Extend,
1007                    _ => return Err(bad()),
1008                }
1009            }
1010            "INITIAL" if sub.is_empty() => self.initial = !off,
1011            "VLR" => {
1012                self.vlr = match sub {
1013                    "STANDARD" | "S" => Vlr::Standard,
1014                    "COMPAT" | "C" => Vlr::Compat,
1015                    _ => return Err(bad()),
1016                }
1017            }
1018            "VSAMOPENFS" => {
1019                self.vsamopenfs = match sub {
1020                    "COMPAT" | "C" => VsamOpenFs::Compat,
1021                    "SUCC" | "S" => VsamOpenFs::Succ,
1022                    _ => return Err(bad()),
1023                }
1024            }
1025            // NOOPTIMIZE, OPTIMIZE, OPTIMIZE(STD) and OPTIMIZE(FULL) are tolerated as Table 51
1026            // maps them; FULL's STGOPT changes nothing ironwork runs.
1027            "OPTIMIZE" => {
1028                self.optimize = match (off, sub) {
1029                    (true, "") | (false, "0") => 0,
1030                    (false, "1") => 1,
1031                    (false, "2" | "" | "STD" | "FULL") => 2,
1032                    _ => return Err(bad()),
1033                }
1034            }
1035            "APOST/QUOTE" | "INITIAL" => return Err(bad()),
1036            _ => return Ok(false),
1037        }
1038        Ok(true)
1039    }
1040
1041    /// Applies a flag of this compiler's own command line.
1042    pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
1043        match flag {
1044            "-silent" => self.trunc_check = TruncCheck::Silent,
1045            "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
1046            "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
1047            "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
1048            "-warnings-block" => self.warnings = Warnings::Block,
1049            "-debug" => self.debug = true,
1050            "--cics-return-warning=once" => self.cics_return_warning = CicsReturnWarning::Once,
1051            "--cics-return-warning=always" => self.cics_return_warning = CicsReturnWarning::Always,
1052            "--cics-return-warning=never" => self.cics_return_warning = CicsReturnWarning::Never,
1053            // The compiler invocation's OPTIMIZE, which a card's outranks.
1054            "--optimize=0" => self.optimize = 0,
1055            "--optimize=1" => self.optimize = 1,
1056            "--optimize=2" => self.optimize = 2,
1057            f if f.starts_with("--compliance=") => match Compliance::named(&f["--compliance=".len()..]) {
1058                Some(c) => self.compliance = c,
1059                None => return Err(OptionError::UnknownFlag(flag.to_owned())),
1060            },
1061            f if f.starts_with("--dialect=") => match Dialect::named(&f["--dialect=".len()..]) {
1062                Some(d) => self.dialect = d,
1063                None => return Err(OptionError::UnknownFlag(flag.to_owned())),
1064            },
1065            "--program-scope=strict" => self.program_scope = ProgramScope::Strict,
1066            "--program-scope=flexible" => self.program_scope = ProgramScope::Flexible,
1067            "--unresolved-calls=run" => self.unresolved_calls = UnresolvedCalls::Run,
1068            "--unresolved-calls=fail" => self.unresolved_calls = UnresolvedCalls::Fail,
1069            "--le-services=programs" => self.le_services = LeServices::Programs,
1070            "--le-services=bind" => self.le_services = LeServices::Bind,
1071            f if f.starts_with("--assume=") => match Assumed::parse(&f["--assume=".len()..]) {
1072                Ok((i, mark)) => self.assumed.given[i] = mark,
1073                Err(_) => return Err(OptionError::UnknownFlag(flag.to_owned())),
1074            },
1075            _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
1076        }
1077        Ok(())
1078    }
1079
1080    /// The COMPILE option in force: a card's, which outranks the command line as a PROCESS
1081    /// statement outranks the compiler's invocation (Programming Guide SC27-8714-03, p. 273), else
1082    /// NOCOMPILE(W) under `-warnings-block`, else IBM's default NOCOMPILE(S).
1083    pub fn object_code(&self) -> Compile {
1084        self.compile.unwrap_or(match self.warnings {
1085            Warnings::Block => Compile::Until(Stop::W),
1086            Warnings::Proceed => Compile::default(),
1087        })
1088    }
1089
1090    /// Whether an unsigned zoned integer compared with zero, or with an unsigned zoned integer of
1091    /// its own length, is compared by its bytes, zones included: under INVDATA(NOFORCENUMCMP)
1092    /// (assumption C223), and under NOINVDATA at OPTIMIZE(1) or OPTIMIZE(2) (C262).
1093    pub fn zones_compared(&self) -> bool {
1094        match self.invdata {
1095            Some(i) => !i.forcenumcmp,
1096            None => self.optimize > 0,
1097        }
1098    }
1099
1100    /// Whether an unsigned zoned integer compared with zero is compared by its bytes: where zones
1101    /// are compared, but under NOINVDATA and C262 gnucobol as a number at every OPTIMIZE
1102    /// level, as cobc compares it (C262).
1103    pub fn zones_compared_with_zero(&self) -> bool {
1104        self.zones_compared() && !self.cobc_zoned_compare()
1105    }
1106
1107    /// Whether an unsigned zoned integer compared with one of its own length is compared by its
1108    /// bytes: where zones are compared, and under NOINVDATA and C262 gnucobol at every
1109    /// OPTIMIZE level, as cobc compares two such items with memcmp (C262).
1110    pub fn zones_compared_between_items(&self) -> bool {
1111        self.zones_compared() || self.cobc_zoned_compare()
1112    }
1113
1114    fn cobc_zoned_compare(&self) -> bool {
1115        self.invdata.is_none() && self.dialect_of(Switched::OptimizedZonesCompared) == Dialect::Gnucobol
1116    }
1117
1118    /// The place in its switch's list of the value a switched assumption takes: `--assume`'s, else
1119    /// the dialect's.
1120    fn switch_value(&self, i: usize) -> usize {
1121        match (self.assumed.given[i], self.dialect) {
1122            (0, Dialect::Ibm) => 0,
1123            (0, Dialect::Gnucobol) => 1,
1124            (mark, _) => usize::from(mark) - 1,
1125        }
1126    }
1127
1128    /// Whose result a switched assumption gives. C101's `off` is [`Options::extra_place`]'s alone.
1129    pub fn dialect_of(&self, switched: Switched) -> Dialect {
1130        if self.switch_value(switched as usize) == 1 { Dialect::Gnucobol } else { Dialect::Ibm }
1131    }
1132
1133    pub fn extra_place(&self) -> ExtraPlace {
1134        match self.switch_value(Switched::RoundedExtraPlace as usize) {
1135            0 => ExtraPlace::Every,
1136            1 => ExtraPlace::Last,
1137            _ => ExtraPlace::Off,
1138        }
1139    }
1140
1141    /// The `--assume` flags that give these options' choices, for a compile that repeats them.
1142    pub fn assume_flags(&self) -> impl Iterator<Item = String> {
1143        SWITCHES.iter().zip(self.assumed.given).filter(|&(_, mark)| mark > 0).map(|((id, values), mark)| format!("--assume={id}={}", values[usize::from(mark) - 1]))
1144    }
1145
1146    /// Each switched assumption whose value in force is not `ibm`, with that value.
1147    pub fn alternatives_in_force(&self) -> impl Iterator<Item = (&'static str, &'static str)> {
1148        SWITCHES.iter().enumerate().map(|(i, (id, values))| (*id, values[self.switch_value(i)])).filter(|&(_, value)| value != "ibm")
1149    }
1150
1151    pub fn code_page(&self) -> &'static CodePage {
1152        CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
1153    }
1154
1155    /// The CURRENCY option's character, a hexadecimal literal's read in the program's code page;
1156    /// Err with that character when the page gives one the option may not name.
1157    pub fn currency_symbol(&self) -> Option<Result<char, char>> {
1158        self.currency.map(|c| match c {
1159            Currency::Char(c) => Ok(c),
1160            Currency::Hex(b) => {
1161                let c = self.code_page().decode_byte(b);
1162                if can_be_currency(c) { Ok(c) } else { Err(c) }
1163            }
1164        })
1165    }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170    use super::*;
1171
1172    #[test]
1173    fn defaults_are_ibms() {
1174        let o = Options::default();
1175        assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
1176        assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
1177    }
1178
1179    #[test]
1180    fn invdata_zonedata_and_zwb() {
1181        let mut o = Options::default();
1182        assert_eq!((o.invdata, o.zwb), (None, true));
1183        assert_eq!(o.apply("INVDATA"), Ok(true));
1184        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: false, cleansign: true }));
1185        assert_eq!(o.apply("INVD(FNC,NOCS)"), Ok(true));
1186        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: false }));
1187        assert_eq!(o.apply("NOINVDATA"), Ok(true));
1188        assert_eq!(o.invdata, None);
1189        assert_eq!(o.apply("ZD(MIG)"), Ok(true));
1190        assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: true }));
1191        assert_eq!(o.apply("ZONEDATA(NOPFD)"), Ok(true));
1192        assert_eq!(o.invdata, Some(Invdata::default()));
1193        assert_eq!(o.apply("ZONEDATA(PFD)"), Ok(true));
1194        assert_eq!(o.invdata, None);
1195        assert!(o.apply("INVDATA(SOMETIMES)").is_err());
1196        assert_eq!(o.apply("NOZWB"), Ok(true));
1197        assert!(!o.zwb);
1198    }
1199
1200    #[test]
1201    fn optimize_levels_and_the_removed_spellings_table_51_maps() {
1202        let mut o = Options::default();
1203        assert_eq!(o.optimize, 0);
1204        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)] {
1205            assert_eq!(o.apply(option), Ok(true), "{option}");
1206            assert_eq!(o.optimize, level, "{option}");
1207        }
1208        assert!(o.apply("OPT(3)").is_err());
1209        assert!(o.apply("NOOPTIMIZE(2)").is_err());
1210        assert_eq!(o.optimize, 2);
1211    }
1212
1213    #[test]
1214    fn zones_are_compared_under_invdata_noforcenumcmp_or_noinvdata_optimized() {
1215        let compared = |options: &[&str]| {
1216            let mut o = Options::default();
1217            options.iter().for_each(|x| assert_eq!(o.apply(x), Ok(true)));
1218            o.zones_compared()
1219        };
1220        assert!(!compared(&[]));
1221        assert!(compared(&["OPT(1)"]));
1222        assert!(compared(&["OPT(2)"]));
1223        assert!(compared(&["INVDATA"]));
1224        assert!(!compared(&["INVDATA(FNC)", "OPT(2)"]));
1225        assert!(!compared(&["ZONEDATA(MIG)", "OPT(2)"]));
1226    }
1227
1228    #[test]
1229    fn dynam_and_its_abbreviations() {
1230        let mut o = Options::default();
1231        assert_eq!(o.apply("DYN"), Ok(true));
1232        assert!(o.dynam);
1233        assert_eq!(o.apply("NODYNAM"), Ok(true));
1234        assert!(!o.dynam);
1235    }
1236
1237    #[test]
1238    fn adv_and_noadv_have_no_abbreviations() {
1239        let mut o = Options::default();
1240        assert_eq!(o.apply("noadv"), Ok(true));
1241        assert!(!o.adv);
1242        assert_eq!(o.apply("ADV"), Ok(true));
1243        assert!(o.adv);
1244        assert_eq!(o.apply("NOAD"), Ok(false));
1245    }
1246
1247    #[test]
1248    fn thread_dll_rent_and_dbcs_and_their_negatives() {
1249        let mut o = Options::default();
1250        for option in ["thread", "DLL", "NORENT", "NODBCS"] {
1251            assert_eq!(o.apply(option), Ok(true));
1252        }
1253        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
1254        for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
1255            assert_eq!(o.apply(option), Ok(true));
1256        }
1257        assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
1258    }
1259
1260    #[test]
1261    fn abbreviations_and_case() {
1262        let mut o = Options::default();
1263        assert_eq!(o.apply("ar(e)"), Ok(true));
1264        assert_eq!(o.apply("CP(1047)"), Ok(true));
1265        assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
1266        assert_eq!(o.apply("fsrt"), Ok(true));
1267        assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
1268        assert_eq!(o.apply("NOFASTSRT"), Ok(true));
1269        assert!(!o.fastsrt);
1270    }
1271
1272    #[test]
1273    fn ibms_table_is_whole_and_no_spelling_names_two_options() {
1274        let all: Vec<Documented> = documented().collect();
1275        assert_eq!(all.len(), 85, "Table 45 lists 85 options");
1276        let mut seen = std::collections::HashMap::new();
1277        for o in &all {
1278            for s in o.spellings() {
1279                assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
1280            }
1281        }
1282        assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
1283        let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
1284        assert!(!adata.process && adata.page == 345);
1285    }
1286
1287    #[test]
1288    fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
1289        let suboption = |name| match name {
1290            "ARITH" => "(E)",
1291            "CODEPAGE" => "(1047)",
1292            "TRUNC" => "(OPT)",
1293            "NUMPROC" => "(PFD)",
1294            "ZONEDATA" => "(MIG)",
1295            _ => "",
1296        };
1297        for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE", "INVDATA", "ZONEDATA", "ZWB", "OPTIMIZE"] {
1298            let o = documented().find(|o| o.name == name).unwrap();
1299            for s in o.spellings() {
1300                assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
1301            }
1302        }
1303        let mut o = Options::default();
1304        o.apply("FSRT").unwrap();
1305        o.apply("NOFSRT").unwrap();
1306        assert!(!o.fastsrt);
1307        assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
1308        assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
1309        assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
1310        assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
1311    }
1312
1313    /// Run with IRONWORK_COBOLWORK_DIR naming a cobolwork checkout to check the vendored copy.
1314    #[test]
1315    fn the_vendored_table_is_cobolworks() {
1316        let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
1317        let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
1318        assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
1319    }
1320
1321    #[test]
1322    fn options_this_layer_does_not_read_pass_through() {
1323        assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
1324    }
1325
1326    #[test]
1327    fn an_option_that_cannot_be_applied_says_why_and_leaves_the_options_as_they_were() {
1328        let mut o = Options::default();
1329        o.apply("NUMPROC(PFD)").unwrap();
1330        o.apply("TRUNC(BIN)").unwrap();
1331        assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
1332        assert_eq!(o.apply("CODEPAGE(290)"), Err(OptionError::UnsupportedCodePage(290)));
1333        assert!(matches!(o.apply("CP(X)"), Err(OptionError::BadSuboption { .. })));
1334        for (option, warns) in [("LIB", false), ("SIZE(MAX)", false), ("sz(2097152)", false), ("FLAGSAA", true), ("NOFDUMP", true)] {
1335            assert!(matches!(o.apply(option), Err(OptionError::NoEffect { warning, .. }) if warning == warns), "{option}");
1336        }
1337        assert_eq!((o.apply("NOLIB"), o.apply("FDUMP")), (Ok(false), Ok(false)));
1338        assert_eq!((o.numproc, o.trunc, o.codepage), (Numproc::Pfd, Trunc::Bin, 1140));
1339        let removed = o.apply("NUMPROC(MIG)").unwrap_err();
1340        assert_eq!(removed.to_string(), "NUMPROC(MIG) was removed in Enterprise COBOL V5, so NUMPROC(NOPFD) is in effect");
1341        assert_eq!(o.numproc, Numproc::Nopfd, "the default NUMPROC, not the one before");
1342    }
1343
1344    #[test]
1345    fn codepage_takes_the_mixed_pages_dbcs_programs_compile_with() {
1346        let mut o = Options::default();
1347        for ccsid in [930, 939, 1390, 1399, 5026, 5035, 933, 1364, 935, 1388, 937] {
1348            o.apply(&format!("CODEPAGE({ccsid})")).unwrap();
1349            assert!(o.code_page().dbcs_ccsid().is_some(), "{ccsid}");
1350        }
1351    }
1352
1353    #[test]
1354    fn silent_flag_turns_off_trunc_reports() {
1355        let mut o = Options::default();
1356        o.apply_flag("-silent").unwrap();
1357        assert_eq!(o.trunc_check, TruncCheck::Silent);
1358        o.apply_flag("-strict-sort-keys").unwrap();
1359        assert_eq!(o.sort_keys, SortKeys::Strict);
1360        assert!(o.apply_flag("-quiet").is_err());
1361    }
1362
1363    #[test]
1364    fn warnings_proceed_unless_the_flag_blocks_them() {
1365        let mut o = Options::default();
1366        assert_eq!(o.warnings, Warnings::Proceed);
1367        o.apply_flag("-warnings-block").unwrap();
1368        assert_eq!(o.warnings, Warnings::Block);
1369        assert!(o.apply_flag("-Werror").is_err());
1370    }
1371
1372    #[test]
1373    fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
1374        let given = |option: &str| {
1375            let mut o = Options::default();
1376            o.apply(option).map(|_| o.compile)
1377        };
1378        assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
1379        assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
1380        assert_eq!(given("c"), Ok(Some(Compile::Full)));
1381        assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
1382        assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
1383        assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
1384        assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
1385        for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
1386            assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1387        }
1388        let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
1389        assert_eq!(codes, [16, 12, 8, 4, 0]);
1390    }
1391
1392    #[test]
1393    fn a_card_outranks_warnings_block() {
1394        let mut o = Options::default();
1395        o.apply_flag("-warnings-block").unwrap();
1396        assert_eq!(o.object_code(), Compile::Until(Stop::W));
1397        o.apply("NOCOMPILE(S)").unwrap();
1398        assert_eq!(o.object_code(), Compile::Until(Stop::S));
1399        o.apply("NOCOMPILE(E)").unwrap();
1400        assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
1401    }
1402
1403    #[test]
1404    fn fastsrt_adv_print_excludes_unless_included() {
1405        let mut o = Options::default();
1406        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
1407        o.apply_flag("--fastsrt-adv-print=include").unwrap();
1408        assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
1409        o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
1410        assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
1411        assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
1412        assert!(o.apply_flag("--fastsrt-adv-print").is_err());
1413    }
1414
1415    #[test]
1416    fn the_options_of_roadmap_2_10_to_2_12_default_to_ibms_and_take_each_documented_suboption() {
1417        let o = Options::default();
1418        assert_eq!((o.quote, o.currency, o.nsymbol, o.dispsign, o.intdate), (Quote::Quote, None, Nsymbol::National, DispSign::Compat, IntDate::Ansi));
1419        assert_eq!((o.qualify, o.initial, o.vlr, o.vsamopenfs), (Qualify::Compat, false, Vlr::Standard, VsamOpenFs::Compat));
1420        let given = |cards: &[&str]| {
1421            let mut o = Options::default();
1422            cards.iter().for_each(|c| assert_eq!(o.apply(c), Ok(true), "{c}"));
1423            o
1424        };
1425        assert_eq!(given(&["APOST"]).quote, Quote::Apost);
1426        assert_eq!(given(&["apost", "Q"]).quote, Quote::Quote);
1427        assert_eq!(given(&["NS(DBCS)"]).nsymbol, Nsymbol::Dbcs);
1428        assert_eq!(given(&["NSYMBOL(DBCS)", "NS(NAT)"]).nsymbol, Nsymbol::National);
1429        assert_eq!(given(&["DS(S)"]).dispsign, DispSign::Sep);
1430        assert_eq!(given(&["DISPSIGN(SEP)", "DISPSIGN(COMPAT)"]).dispsign, DispSign::Compat);
1431        assert_eq!(given(&["INTDATE(LILIAN)"]).intdate, IntDate::Lilian);
1432        let cards = |c: &[&str]| Pgmname::of(&c.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>());
1433        assert_eq!(cards(&[]), Pgmname::Compat);
1434        assert_eq!(cards(&["PGMN(LM)"]), Pgmname::LongMixed);
1435        assert_eq!(cards(&["PGMNAME(LONGUPPER)", "PGMNAME(U)", "PGMNAME(X)"]), Pgmname::LongUpper);
1436        assert_eq!(given(&["QUA(E)"]).qualify, Qualify::Extend);
1437        assert_eq!(given(&["QUALIFY(EXTEND)", "QUA(C)"]).qualify, Qualify::Compat);
1438        assert!(given(&["INITIAL"]).initial);
1439        assert!(!given(&["INITIAL", "NOINITIAL"]).initial);
1440        assert_eq!(given(&["VLR(C)"]).vlr, Vlr::Compat);
1441        assert_eq!(given(&["VLR(COMPAT)", "VLR(STANDARD)"]).vlr, Vlr::Standard);
1442        assert_eq!(given(&["VS(S)"]).vsamopenfs, VsamOpenFs::Succ);
1443        assert_eq!(given(&["VSAMOPENFS(SUCC)", "VSAMOPENFS(COMPAT)"]).vsamopenfs, VsamOpenFs::Compat);
1444        for bad in ["APOST(X)", "NSYMBOL(N)", "DS(X)", "INTDATE(JULIAN)", "QUA(X)", "INITIAL(Y)", "VLR(X)", "VS(X)", "INTDATE", "PGMNAME(X)", "PGMNAME"] {
1445            assert!(matches!(Options::default().apply(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1446        }
1447    }
1448
1449    #[test]
1450    fn currency_takes_one_character_it_may_name_in_either_delimiter_or_a_hexadecimal_byte() {
1451        let currency = |card: &str| {
1452            let mut o = Options::default();
1453            o.apply(card).map(|_| o.currency)
1454        };
1455        assert_eq!(currency("CURRENCY('£')"), Ok(Some(Currency::Char('£'))));
1456        assert_eq!(currency("curr(\"f\")"), Ok(Some(Currency::Char('f'))), "the literal keeps its case");
1457        assert_eq!(currency("CURRENCY(X'5B')"), Ok(Some(Currency::Hex(0x5B))));
1458        assert_eq!(currency("NOCURR"), Ok(None));
1459        for bad in ["CURRENCY('E')", "CURRENCY('e')", "CURRENCY('1')", "CURRENCY(' ')", "CURRENCY('*')", "CURRENCY('EUR')", "CURRENCY(SPACE)", "CURRENCY(N'£')", "CURRENCY(Z'£')", "CURRENCY(X'5B5B')", "CURRENCY", "NOCURRENCY('£')"] {
1460            assert!(matches!(currency(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1461        }
1462        let mut o = Options::default();
1463        o.apply("CURRENCY(X'4A')").unwrap();
1464        assert_eq!(o.currency_symbol(), Some(Ok('¢')), "X'4A' is the cent sign in CCSID 1140");
1465        o.apply("CURRENCY(X'F1')").unwrap();
1466        assert_eq!(o.currency_symbol(), Some(Err('1')));
1467        assert_eq!(Options::default().currency_symbol(), None);
1468    }
1469
1470    #[test]
1471    fn numcheck_takes_ibms_suboption_defaults_and_zonecheck_is_its_zoned_check() {
1472        let numcheck = |card: &str| {
1473            let mut o = Options::default();
1474            o.apply(card).map(|_| o.numcheck)
1475        };
1476        let all = Numcheck::default();
1477        assert_eq!(all, Numcheck { zon: Some(ZonCheck { alphnum: true, lax: false }), pac: true, bin: Some(BinCheck { truncbin: true }), abd: false });
1478        assert_eq!(numcheck("NUMCHECK"), Ok(Some(all)));
1479        assert_eq!(numcheck("NC(ABD)"), Ok(Some(Numcheck { abd: true, ..all })));
1480        assert_eq!(numcheck("NUMCHECK(BIN)"), Ok(Some(Numcheck { zon: None, pac: false, bin: Some(BinCheck { truncbin: true }), abd: false })));
1481        assert_eq!(
1482            numcheck("NUMCHECK(ZON(NOALPHNUM,LAX),NOPAC,BIN(NOTRUNCBIN),ABD)"),
1483            Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: false, lax: true }), pac: false, bin: Some(BinCheck { truncbin: false }), abd: true }))
1484        );
1485        assert_eq!(numcheck("NUMCHECK(ZON(LAXREDEF))"), Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: true, lax: true }), pac: false, bin: None, abd: false })));
1486        assert_eq!(numcheck("NUMCHECK(NOZON,NOPAC,NOBIN)"), Ok(None));
1487        assert_eq!(numcheck("NONC"), Ok(None));
1488        for bad in ["NUMCHECK(ZON(X))", "NUMCHECK(PAC(X))", "NUMCHECK(BIN(X))", "NUMCHECK(X)", "ZONECHECK", "ZC(X)"] {
1489            assert!(matches!(numcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1490        }
1491        assert_eq!(numcheck("ZC(ABD)"), Ok(Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd: true })));
1492        let mut o = Options::default();
1493        o.apply("NUMCHECK").unwrap();
1494        o.apply("NOZONECHECK").unwrap();
1495        assert_eq!(o.numcheck, Some(Numcheck { zon: None, ..all }));
1496        o.apply("ZONECHECK(MSG)").unwrap();
1497        o.apply("NOZC").unwrap();
1498        assert_eq!(o.numcheck, None);
1499    }
1500
1501    #[test]
1502    fn parmcheck_and_initcheck_take_their_suboptions_and_ibms_defaults() {
1503        let parmcheck = |card: &str| {
1504            let mut o = Options::default();
1505            o.apply(card).map(|_| o.parmcheck)
1506        };
1507        assert_eq!(parmcheck("PARMCHECK"), Ok(Some(Parmcheck { abd: false, bytes: 100 })));
1508        assert_eq!(parmcheck("PC(ABD)"), Ok(Some(Parmcheck { abd: true, bytes: 100 })));
1509        assert_eq!(parmcheck("PC(5000)"), Ok(Some(Parmcheck { abd: false, bytes: 5000 })));
1510        assert_eq!(parmcheck("PARMCHECK(ABD,1)"), Ok(Some(Parmcheck { abd: true, bytes: 1 })));
1511        assert_eq!(parmcheck("NOPC"), Ok(None));
1512        for bad in ["PC(0)", "PC(10000)", "PC(5000,ABD)", "PC(MSG,1,2)", "PC(X)"] {
1513            assert!(matches!(parmcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1514        }
1515        let initcheck = |card: &str| {
1516            let mut o = Options::default();
1517            o.apply(card).map(|_| o.initcheck)
1518        };
1519        assert_eq!(initcheck("INITCHECK"), Ok(Some(Initcheck::Lax)));
1520        assert_eq!(initcheck("IC(STRICT)"), Ok(Some(Initcheck::Strict)));
1521        assert_eq!(initcheck("NOIC"), Ok(None));
1522        assert!(matches!(initcheck("IC(X)"), Err(OptionError::BadSuboption { .. })));
1523        let o = Options::default();
1524        assert_eq!((o.numcheck, o.parmcheck, o.initcheck), (None, None, None));
1525    }
1526
1527    #[test]
1528    fn cics_return_warning_is_once_unless_the_flag_says_always_or_never() {
1529        let mut o = Options::default();
1530        assert_eq!(o.cics_return_warning, CicsReturnWarning::Once);
1531        for mode in [CicsReturnWarning::Always, CicsReturnWarning::Never, CicsReturnWarning::Once] {
1532            o.apply_flag(mode.flag()).unwrap();
1533            assert_eq!(o.cics_return_warning, mode);
1534        }
1535        assert!(o.apply_flag("--cics-return-warning=sometimes").is_err());
1536        assert!(o.apply_flag("--cics-return-warning").is_err());
1537    }
1538
1539    #[test]
1540    fn compliance_is_strict_unless_the_flag_says_extended() {
1541        let mut o = Options::default();
1542        assert_eq!(o.compliance, Compliance::Strict);
1543        for level in [Compliance::Extended, Compliance::Strict] {
1544            o.apply_flag(level.flag()).unwrap();
1545            assert_eq!(o.compliance, level);
1546            assert_eq!(Compliance::named(level.name()), Some(level));
1547        }
1548        for bad in ["--compliance=EXTENDED", "--compliance=", "--compliance", "--compliance=mf"] {
1549            assert!(o.apply_flag(bad).is_err(), "{bad}");
1550        }
1551        let flags = |given: &[&str]| Compliance::of(&given.iter().map(|f| f.to_string()).collect::<Vec<_>>());
1552        assert_eq!(flags(&[]), Compliance::Strict);
1553        assert_eq!(flags(&["-silent", "--compliance=extended"]), Compliance::Extended);
1554        assert_eq!(flags(&["--compliance=extended", "--compliance=strict"]), Compliance::Strict);
1555    }
1556
1557    #[test]
1558    fn the_dialect_is_ibm_unless_the_flag_says_gnucobol() {
1559        let mut o = Options::default();
1560        assert_eq!(o.dialect, Dialect::Ibm);
1561        for dialect in [Dialect::Gnucobol, Dialect::Ibm] {
1562            o.apply_flag(dialect.flag()).unwrap();
1563            assert_eq!(o.dialect, dialect);
1564            assert_eq!(Dialect::named(dialect.name()), Some(dialect));
1565        }
1566        for bad in ["--dialect=GNUCOBOL", "--dialect=", "--dialect", "--dialect=mf"] {
1567            assert!(o.apply_flag(bad).is_err(), "{bad}");
1568        }
1569        assert_eq!(o.dialect, Dialect::Ibm);
1570    }
1571
1572    #[test]
1573    fn each_switch_names_its_assumption_in_the_order_switched_lists_them() {
1574        use assumptions::*;
1575        let ids = [ROUNDED_EXTRA_PLACE, DISPLAY_OF_NONDISPLAY_NUMERIC, DECIMAL_COMMA_DISPLAY_LITERAL, ACCEPT_AT_END, ENTRY_CALLS, EXTERNAL_STORAGE, OPTIMIZED_ZONES_COMPARED];
1576        let switched = [
1577            Switched::RoundedExtraPlace,
1578            Switched::DisplayOfNondisplayNumeric,
1579            Switched::DecimalCommaDisplayLiteral,
1580            Switched::AcceptAtEnd,
1581            Switched::EntryCalls,
1582            Switched::ExternalStorage,
1583            Switched::OptimizedZonesCompared,
1584        ];
1585        for (s, id) in switched.into_iter().zip(ids) {
1586            let (switch, values) = SWITCHES[s as usize];
1587            assert_eq!(switch, id);
1588            assert_eq!(values[..2], ["ibm", "gnucobol"], "{id}");
1589            assert_eq!(get(id).basis, Basis::Chosen, "{id}");
1590        }
1591    }
1592
1593    #[test]
1594    fn assume_switches_one_assumption_and_wins_over_the_dialect_whatever_the_order() {
1595        let mut o = Options::default();
1596        assert_eq!((o.extra_place(), o.dialect_of(Switched::AcceptAtEnd)), (ExtraPlace::Every, Dialect::Ibm));
1597        o.apply_flag("--assume=C15=gnucobol").unwrap();
1598        assert_eq!((o.extra_place(), o.dialect_of(Switched::AcceptAtEnd)), (ExtraPlace::Every, Dialect::Gnucobol));
1599        o.apply_flag("--dialect=gnucobol").unwrap();
1600        o.apply_flag("--assume=C101=off").unwrap();
1601        assert_eq!((o.extra_place(), o.dialect_of(Switched::EntryCalls)), (ExtraPlace::Off, Dialect::Gnucobol));
1602        o.apply_flag("--assume=C101=ibm").unwrap();
1603        assert_eq!(o.extra_place(), ExtraPlace::Every);
1604        assert_eq!(o.assume_flags().collect::<Vec<_>>(), ["--assume=C101=ibm", "--assume=C15=gnucobol"]);
1605        assert_eq!(o.alternatives_in_force().map(|(id, _)| id).collect::<Vec<_>>(), ["C14", "C95", "C15", "C51", "C180", "C262"]);
1606        o.apply_flag("--dialect=ibm").unwrap();
1607        assert_eq!(o.alternatives_in_force().collect::<Vec<_>>(), [("C15", "gnucobol")]);
1608        assert!(o.apply_flag("--assume=C101=on").is_err());
1609        assert_eq!(o.extra_place(), ExtraPlace::Every);
1610    }
1611
1612    #[test]
1613    fn assume_refuses_by_name_what_it_cannot_switch() {
1614        assert_eq!(Assumed::parse("C101=off"), Ok((0, 3)));
1615        assert_eq!(Assumed::parse("C262=ibm"), Ok((6, 1)));
1616        for (spec, said) in [
1617            ("C101", "--assume C101: needs ID=VALUE, such as C101=off"),
1618            ("C1=gnucobol", "--assume C1=gnucobol: assumption C1 (documented) has no alternative; --assume switches C101, C14, C95, C15, C51, C180 and C262"),
1619            ("C16=gnucobol", "--assume C16=gnucobol: assumption C16 (chosen) has no alternative; --assume switches C101, C14, C95, C15, C51, C180 and C262"),
1620            ("C9999=off", "--assume C9999=off: the register has no assumption C9999; ironwork assumptions lists them"),
1621            ("C14=off", "--assume C14=off: C14 takes ibm or gnucobol"),
1622            ("C101=GNUCOBOL", "--assume C101=GNUCOBOL: C101 takes ibm, gnucobol or off"),
1623        ] {
1624            assert_eq!(Assumed::parse(spec), Err(said.to_owned()), "{spec}");
1625        }
1626    }
1627
1628    #[test]
1629    fn pgmname_forms_the_external_name_as_the_programming_guide_lists() {
1630        assert_eq!(Pgmname::Compat.external("docalc"), "DOCALC");
1631        assert_eq!(Pgmname::Compat.external("Get-Record-Now"), "GET0RECO");
1632        assert_eq!(Pgmname::Compat.external("1st-pass"), "AST0PASS");
1633        assert_eq!(Pgmname::Compat.external("0abc"), "JABC");
1634        assert_eq!(Pgmname::Compat.external("_under"), "_UNDER");
1635        assert_eq!(Pgmname::LongUpper.external("Get-Record-Now"), "GET0RECORD0NOW");
1636        assert_eq!(Pgmname::LongMixed.external("Get-Record-Now"), "Get-Record-Now");
1637    }
1638}