1use std::fmt;
2use zarch::ebcdic::{self, CodePage};
3use zarch::hfp::Precision;
4
5const TABLE: &str = include_str!("../data/enterprise-options.tsv");
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct Documented {
12 pub name: &'static str,
13 spellings: &'static str,
14 pub process: bool,
16 pub first_program_only: bool,
18 pub installation_default: bool,
19 pub page: u16,
21}
22
23impl Documented {
24 pub fn spellings(&self) -> impl Iterator<Item = &'static str> {
25 self.spellings.split(' ')
26 }
27}
28
29pub 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
38pub 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
45pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
102pub enum TruncCheck {
103 #[default]
104 Report,
105 Silent,
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
112pub enum SortKeys {
113 #[default]
114 Dfsort,
115 Strict,
116}
117
118#[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#[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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct BinCheck {
188 pub truncbin: bool,
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct Parmcheck {
196 pub abd: bool,
197 pub bytes: u16,
198}
199
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
204pub enum Initcheck {
205 #[default]
206 Lax,
207 Strict,
208}
209
210#[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
234pub enum Quote {
235 #[default]
236 Quote,
237 Apost,
238}
239
240impl Quote {
241 pub const fn byte(self) -> u8 {
243 match self {
244 Self::Quote => ebcdic::QUOTE,
245 Self::Apost => ebcdic::APOSTROPHE,
246 }
247 }
248
249 pub const fn unit(self) -> u16 {
251 match self {
252 Self::Quote => 0x0022,
253 Self::Apost => 0x0027,
254 }
255 }
256}
257
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261pub enum Currency {
262 Char(char),
263 Hex(u8),
264}
265
266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub enum Nsymbol {
270 #[default]
271 National,
272 Dbcs,
273}
274
275#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
279pub enum DispSign {
280 #[default]
281 Compat,
282 Sep,
283}
284
285#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
288pub enum IntDate {
289 #[default]
290 Ansi,
291 Lilian,
292}
293
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
297pub enum Qualify {
298 #[default]
299 Compat,
300 Extend,
301}
302
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
307pub enum Vlr {
308 #[default]
309 Standard,
310 Compat,
311}
312
313#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
317pub enum VsamOpenFs {
318 #[default]
319 Compat,
320 Succ,
321}
322
323#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
326pub enum Warnings {
327 #[default]
328 Proceed,
329 Block,
330}
331
332#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub enum Compile {
368 Full,
370 Until(Stop),
372 SyntaxOnly,
374}
375
376#[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 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#[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 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 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 pub fastsrt: bool,
449 pub fastsrt_adv_print: FastsrtAdvPrint,
450 pub sort_keys: SortKeys,
451 pub adv: bool,
454 pub thread: bool,
456 pub dll: bool,
457 pub rent: bool,
458 pub dbcs: bool,
459 pub warnings: Warnings,
460 pub compile: Option<Compile>,
462 pub dynam: bool,
464 pub debug: bool,
467 pub cics_return_warning: CicsReturnWarning,
468 pub invdata: Option<Invdata>,
470 pub zwb: bool,
472 pub quote: Quote,
473 pub currency: Option<Currency>,
476 pub nsymbol: Nsymbol,
477 pub dispsign: DispSign,
478 pub intdate: IntDate,
479 pub qualify: Qualify,
480 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 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#[derive(Clone, Debug, PartialEq, Eq)]
540pub enum OptionError {
541 BadSuboption { option: String, given: String },
543 Removed { option: String, since: &'static str, instead: &'static str },
545 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
564fn 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
578fn 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
597fn inner(option: &str) -> &str {
599 option.split_once('(').map_or("", |(_, rest)| rest.trim_end().strip_suffix(')').unwrap_or(rest)).trim()
600}
601
602fn 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
620fn 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
672fn 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
690pub 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 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 "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 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 "--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 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 pub fn zones_compared(&self) -> bool {
917 match self.invdata {
918 Some(i) => !i.forcenumcmp,
919 None => self.optimize > 0,
920 }
921 }
922
923 pub fn zones_compared_with_zero(&self) -> bool {
927 self.zones_compared() && !self.cobc_zoned_compare()
928 }
929
930 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 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 #[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}