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, PartialEq, Eq)]
335pub enum Compile {
336 Full,
338 Until(Stop),
340 SyntaxOnly,
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
346pub enum Stop {
347 W,
348 E,
349 S,
350}
351
352impl Default for Compile {
353 fn default() -> Self {
354 Self::Until(Stop::S)
355 }
356}
357
358impl Compile {
359 pub const fn stops_at(self) -> u8 {
362 match self {
363 Self::Full => 16,
364 Self::Until(Stop::S) => 12,
365 Self::Until(Stop::E) => 8,
366 Self::Until(Stop::W) => 4,
367 Self::SyntaxOnly => 0,
368 }
369 }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub struct Options {
374 pub arith: Arith,
375 pub trunc: Trunc,
376 pub numproc: Numproc,
377 pub codepage: u16,
378 pub trunc_check: TruncCheck,
379 pub fastsrt: bool,
381 pub fastsrt_adv_print: FastsrtAdvPrint,
382 pub sort_keys: SortKeys,
383 pub adv: bool,
386 pub thread: bool,
388 pub dll: bool,
389 pub rent: bool,
390 pub dbcs: bool,
391 pub warnings: Warnings,
392 pub compile: Option<Compile>,
394 pub dynam: bool,
396 pub debug: bool,
399 pub cics_return_warning: CicsReturnWarning,
400 pub invdata: Option<Invdata>,
402 pub zwb: bool,
404 pub quote: Quote,
405 pub currency: Option<Currency>,
408 pub nsymbol: Nsymbol,
409 pub dispsign: DispSign,
410 pub intdate: IntDate,
411 pub qualify: Qualify,
412 pub initial: bool,
416 pub vlr: Vlr,
417 pub vsamopenfs: VsamOpenFs,
418 pub numcheck: Option<Numcheck>,
419 pub parmcheck: Option<Parmcheck>,
420 pub initcheck: Option<Initcheck>,
421}
422
423impl Default for Options {
424 fn default() -> Self {
425 Self {
426 arith: Arith::default(),
427 trunc: Trunc::default(),
428 numproc: Numproc::default(),
429 codepage: 1140,
430 trunc_check: TruncCheck::default(),
431 fastsrt: false,
432 fastsrt_adv_print: FastsrtAdvPrint::default(),
433 sort_keys: SortKeys::default(),
434 adv: true,
435 thread: false,
436 dll: false,
437 rent: true,
438 dbcs: true,
439 warnings: Warnings::default(),
440 compile: None,
441 dynam: false,
442 debug: false,
443 cics_return_warning: CicsReturnWarning::default(),
444 invdata: None,
445 zwb: true,
446 quote: Quote::default(),
447 currency: None,
448 nsymbol: Nsymbol::default(),
449 dispsign: DispSign::default(),
450 intdate: IntDate::default(),
451 qualify: Qualify::default(),
452 initial: false,
453 vlr: Vlr::default(),
454 vsamopenfs: VsamOpenFs::default(),
455 numcheck: None,
456 parmcheck: None,
457 initcheck: None,
458 }
459 }
460}
461
462#[derive(Clone, Debug, PartialEq, Eq)]
465pub enum OptionError {
466 BadSuboption { option: String, given: String },
468 Removed { option: String, since: &'static str, instead: &'static str },
470 NoEffect { option: &'static str, why: &'static str, warning: bool },
473 UnsupportedCodePage(u16),
474 UnknownFlag(String),
475}
476
477impl fmt::Display for OptionError {
478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479 match self {
480 Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
481 Self::Removed { option, since, instead } => write!(f, "{option} was removed in Enterprise COBOL {since}, so {instead} is in effect"),
482 Self::NoEffect { option, why, .. } => write!(f, "{option} {why}"),
483 Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not a single-byte EBCDIC page this compiler carries"),
484 Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
485 }
486 }
487}
488
489fn without_effect(name: &str) -> Option<OptionError> {
493 let (option, why, warning) = match name {
494 "LIB" => ("LIB", "is no longer needed: COPY members are always read from the libraries", false),
495 "SIZE" | "SZ" => ("SIZE", "was removed in Enterprise COBOL V5 and has no effect", false),
496 "FLAGSAA" => ("FLAGSAA", "is not an Enterprise COBOL option and has no effect", true),
497 "NOFDUMP" => ("NOFDUMP", "is not an Enterprise COBOL option and has no effect", true),
498 _ => return None,
499 };
500 Some(OptionError::NoEffect { option, why, warning })
501}
502
503fn currency_literal(text: &str) -> Option<Currency> {
507 fn quoted(t: &str) -> Option<&str> {
508 let q = t.chars().next().filter(|q| matches!(q, '\'' | '"'))?;
509 (t.len() >= 2 && t.ends_with(q)).then(|| &t[1..t.len() - 1])
510 }
511 let text = text.trim();
512 if let Some(hex) = text.strip_prefix(['X', 'x']).and_then(quoted) {
513 return u8::from_str_radix(hex, 16).ok().filter(|_| hex.len() == 2).map(Currency::Hex);
514 }
515 let mut chars = quoted(text)?.chars();
516 match (chars.next(), chars.next()) {
517 (Some(c), None) if can_be_currency(c) => Some(Currency::Char(c)),
518 _ => None,
519 }
520}
521
522fn inner(option: &str) -> &str {
524 option.split_once('(').map_or("", |(_, rest)| rest.trim_end().strip_suffix(')').unwrap_or(rest)).trim()
525}
526
527fn top_level(text: &str) -> Vec<&str> {
529 let (mut parts, mut depth, mut start) = (Vec::new(), 0i32, 0);
530 for (i, c) in text.char_indices() {
531 match c {
532 '(' => depth += 1,
533 ')' => depth -= 1,
534 ',' if depth == 0 => {
535 parts.push(text[start..i].trim());
536 start = i + 1;
537 }
538 _ => {}
539 }
540 }
541 parts.push(text[start..].trim());
542 parts.into_iter().filter(|p| !p.is_empty()).collect()
543}
544
545fn numcheck(text: &str) -> Option<Option<Numcheck>> {
549 let (mut zon, mut pac, mut bin, mut abd, mut typed) = (None, None, None, false, false);
550 for part in top_level(text) {
551 let (word, args) = match part.split_once('(') {
552 Some((w, _)) => (w.trim(), top_level(inner(part))),
553 None => (part, Vec::new()),
554 };
555 match word {
556 "ZON" => {
557 let mut z = ZonCheck::default();
558 for a in args {
559 match a {
560 "ALPHNUM" => z.alphnum = true,
561 "NOALPHNUM" => z.alphnum = false,
562 "LAX" | "LAXREDEF" => z.lax = true,
563 "STRICT" | "STRICTREDEF" => z.lax = false,
564 _ => return None,
565 }
566 }
567 zon = Some(Some(z));
568 }
569 "NOZON" if args.is_empty() => zon = Some(None),
570 "PAC" if args.is_empty() => pac = Some(true),
571 "NOPAC" if args.is_empty() => pac = Some(false),
572 "BIN" => {
573 bin = Some(Some(BinCheck {
574 truncbin: match args[..] {
575 [] | ["TRUNCBIN"] => true,
576 ["NOTRUNCBIN"] => false,
577 _ => return None,
578 },
579 }))
580 }
581 "NOBIN" if args.is_empty() => bin = Some(None),
582 "MSG" if args.is_empty() => abd = false,
583 "ABD" if args.is_empty() => abd = true,
584 _ => return None,
585 }
586 typed |= matches!(word, "ZON" | "NOZON" | "PAC" | "NOPAC" | "BIN" | "NOBIN");
587 }
588 let all = Numcheck::default();
589 let n = if typed {
590 Numcheck { zon: zon.flatten(), pac: pac.unwrap_or(false), bin: bin.flatten(), abd }
591 } else {
592 Numcheck { abd, ..all }
593 };
594 Some((n.zon.is_some() || n.pac || n.bin.is_some()).then_some(n))
595}
596
597fn parmcheck(sub: &str) -> Option<Parmcheck> {
600 let mut p = Parmcheck { abd: false, bytes: 100 };
601 let parts: Vec<&str> = sub.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
602 if parts.len() > 2 {
603 return None;
604 }
605 for (i, part) in parts.iter().enumerate() {
606 match *part {
607 "MSG" if i == 0 => p.abd = false,
608 "ABD" if i == 0 => p.abd = true,
609 n => p.bytes = n.parse().ok().filter(|b| (1..=9999).contains(b))?,
610 }
611 }
612 Some(p)
613}
614
615pub fn can_be_currency(c: char) -> bool {
618 u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz *+-/,.;()\"=".contains(c)
619}
620
621impl std::error::Error for OptionError {}
622
623impl Options {
624 pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
628 let given = option.trim();
629 let option = given.to_ascii_uppercase();
630 let (name, sub) = match option.split_once('(') {
631 Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
632 None => (option.as_str(), ""),
633 };
634 let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
635 if let Some(e) = without_effect(name) {
636 return Err(e);
637 }
638 let Some((documented, off)) = spelled(name) else { return Ok(false) };
639 match documented.name {
640 "ARITH" => {
641 self.arith = match sub {
642 "COMPAT" | "C" => Arith::Compat,
643 "EXTEND" | "E" => Arith::Extend,
644 _ => return Err(bad()),
645 }
646 }
647 "TRUNC" => {
648 self.trunc = match sub {
649 "STD" => Trunc::Std,
650 "OPT" => Trunc::Opt,
651 "BIN" => Trunc::Bin,
652 _ => return Err(bad()),
653 }
654 }
655 "NUMPROC" => {
656 self.numproc = match sub {
657 "NOPFD" => Numproc::Nopfd,
658 "PFD" => Numproc::Pfd,
659 "MIG" => {
660 self.numproc = Numproc::default();
661 return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5", instead: "NUMPROC(NOPFD)" });
662 }
663 _ => return Err(bad()),
664 }
665 }
666 "CODEPAGE" => {
667 let ccsid: u16 = sub.parse().map_err(|_| bad())?;
668 CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
669 self.codepage = ccsid;
670 }
671 "COMPILE" => {
672 self.compile = Some(match (off, sub) {
673 (false, "") => Compile::Full,
674 (true, "") => Compile::SyntaxOnly,
675 (true, "W") => Compile::Until(Stop::W),
676 (true, "E") => Compile::Until(Stop::E),
677 (true, "S") => Compile::Until(Stop::S),
678 _ => return Err(bad()),
679 })
680 }
681 "INVDATA" if off => self.invdata = None,
682 "INVDATA" => {
683 let mut invdata = Invdata::default();
684 for part in sub.split(',').map(str::trim).filter(|p| !p.is_empty()) {
685 match part {
686 "FORCENUMCMP" | "FNC" => invdata.forcenumcmp = true,
687 "NOFORCENUMCMP" | "NOFNC" => invdata.forcenumcmp = false,
688 "CLEANSIGN" | "CS" => invdata.cleansign = true,
689 "NOCLEANSIGN" | "NOCS" => invdata.cleansign = false,
690 _ => return Err(bad()),
691 }
692 }
693 self.invdata = Some(invdata);
694 }
695 "ZONEDATA" => {
696 self.invdata = match sub {
697 "PFD" => None,
698 "NOPFD" => Some(Invdata::default()),
699 "MIG" => Some(Invdata { forcenumcmp: true, cleansign: true }),
700 _ => return Err(bad()),
701 }
702 }
703 "ZWB" => self.zwb = !off,
704 "NUMCHECK" if off => self.numcheck = None,
705 "NUMCHECK" => self.numcheck = numcheck(inner(&option)).ok_or_else(bad)?,
706 "ZONECHECK" if off => self.numcheck = self.numcheck.map(|n| Numcheck { zon: None, ..n }).filter(|n| n.pac || n.bin.is_some()),
707 "ZONECHECK" => {
708 let abd = match sub {
709 "MSG" => false,
710 "ABD" => true,
711 _ => return Err(bad()),
712 };
713 self.numcheck = Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd });
714 }
715 "PARMCHECK" if off => self.parmcheck = None,
716 "PARMCHECK" => self.parmcheck = Some(parmcheck(sub).ok_or_else(bad)?),
717 "INITCHECK" if off => self.initcheck = None,
718 "INITCHECK" => {
719 self.initcheck = Some(match sub {
720 "" | "LAX" => Initcheck::Lax,
721 "STRICT" => Initcheck::Strict,
722 _ => return Err(bad()),
723 })
724 }
725 "FASTSRT" => self.fastsrt = !off,
726 "ADV" => self.adv = !off,
727 "THREAD" => self.thread = !off,
728 "DLL" => self.dll = !off,
729 "RENT" => self.rent = !off,
730 "DBCS" => self.dbcs = !off,
731 "DYNAM" => self.dynam = !off,
732 "APOST/QUOTE" if sub.is_empty() => self.quote = if name == "APOST" { Quote::Apost } else { Quote::Quote },
733 "CURRENCY" => {
734 self.currency = match (off, sub) {
735 (true, "") => None,
736 (false, _) => Some(currency_literal(given.split_once('(').map_or("", |(_, rest)| rest.trim_end().trim_end_matches(')'))).ok_or_else(bad)?),
737 _ => return Err(bad()),
738 }
739 }
740 "NSYMBOL" => {
741 self.nsymbol = match sub {
742 "NATIONAL" | "NAT" => Nsymbol::National,
743 "DBCS" => Nsymbol::Dbcs,
744 _ => return Err(bad()),
745 }
746 }
747 "DISPSIGN" => {
748 self.dispsign = match sub {
749 "COMPAT" | "C" => DispSign::Compat,
750 "SEP" | "S" => DispSign::Sep,
751 _ => return Err(bad()),
752 }
753 }
754 "INTDATE" => {
755 self.intdate = match sub {
756 "ANSI" => IntDate::Ansi,
757 "LILIAN" => IntDate::Lilian,
758 _ => return Err(bad()),
759 }
760 }
761 "QUALIFY" => {
762 self.qualify = match sub {
763 "COMPAT" | "C" => Qualify::Compat,
764 "EXTEND" | "E" => Qualify::Extend,
765 _ => return Err(bad()),
766 }
767 }
768 "INITIAL" if sub.is_empty() => self.initial = !off,
769 "VLR" => {
770 self.vlr = match sub {
771 "STANDARD" | "S" => Vlr::Standard,
772 "COMPAT" | "C" => Vlr::Compat,
773 _ => return Err(bad()),
774 }
775 }
776 "VSAMOPENFS" => {
777 self.vsamopenfs = match sub {
778 "COMPAT" | "C" => VsamOpenFs::Compat,
779 "SUCC" | "S" => VsamOpenFs::Succ,
780 _ => return Err(bad()),
781 }
782 }
783 "APOST/QUOTE" | "INITIAL" => return Err(bad()),
784 _ => return Ok(false),
785 }
786 Ok(true)
787 }
788
789 pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
791 match flag {
792 "-silent" => self.trunc_check = TruncCheck::Silent,
793 "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
794 "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
795 "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
796 "-warnings-block" => self.warnings = Warnings::Block,
797 "-debug" => self.debug = true,
798 "--cics-return-warning=once" => self.cics_return_warning = CicsReturnWarning::Once,
799 "--cics-return-warning=always" => self.cics_return_warning = CicsReturnWarning::Always,
800 "--cics-return-warning=never" => self.cics_return_warning = CicsReturnWarning::Never,
801 _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
802 }
803 Ok(())
804 }
805
806 pub fn object_code(&self) -> Compile {
810 self.compile.unwrap_or(match self.warnings {
811 Warnings::Block => Compile::Until(Stop::W),
812 Warnings::Proceed => Compile::default(),
813 })
814 }
815
816 pub fn code_page(&self) -> &'static CodePage {
817 CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
818 }
819
820 pub fn currency_symbol(&self) -> Option<Result<char, char>> {
823 self.currency.map(|c| match c {
824 Currency::Char(c) => Ok(c),
825 Currency::Hex(b) => {
826 let c = self.code_page().decode_byte(b);
827 if can_be_currency(c) { Ok(c) } else { Err(c) }
828 }
829 })
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn defaults_are_ibms() {
839 let o = Options::default();
840 assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
841 assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
842 }
843
844 #[test]
845 fn invdata_zonedata_and_zwb() {
846 let mut o = Options::default();
847 assert_eq!((o.invdata, o.zwb), (None, true));
848 assert_eq!(o.apply("INVDATA"), Ok(true));
849 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: false, cleansign: true }));
850 assert_eq!(o.apply("INVD(FNC,NOCS)"), Ok(true));
851 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: false }));
852 assert_eq!(o.apply("NOINVDATA"), Ok(true));
853 assert_eq!(o.invdata, None);
854 assert_eq!(o.apply("ZD(MIG)"), Ok(true));
855 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: true }));
856 assert_eq!(o.apply("ZONEDATA(NOPFD)"), Ok(true));
857 assert_eq!(o.invdata, Some(Invdata::default()));
858 assert_eq!(o.apply("ZONEDATA(PFD)"), Ok(true));
859 assert_eq!(o.invdata, None);
860 assert!(o.apply("INVDATA(SOMETIMES)").is_err());
861 assert_eq!(o.apply("NOZWB"), Ok(true));
862 assert!(!o.zwb);
863 }
864
865 #[test]
866 fn dynam_and_its_abbreviations() {
867 let mut o = Options::default();
868 assert_eq!(o.apply("DYN"), Ok(true));
869 assert!(o.dynam);
870 assert_eq!(o.apply("NODYNAM"), Ok(true));
871 assert!(!o.dynam);
872 }
873
874 #[test]
875 fn adv_and_noadv_have_no_abbreviations() {
876 let mut o = Options::default();
877 assert_eq!(o.apply("noadv"), Ok(true));
878 assert!(!o.adv);
879 assert_eq!(o.apply("ADV"), Ok(true));
880 assert!(o.adv);
881 assert_eq!(o.apply("NOAD"), Ok(false));
882 }
883
884 #[test]
885 fn thread_dll_rent_and_dbcs_and_their_negatives() {
886 let mut o = Options::default();
887 for option in ["thread", "DLL", "NORENT", "NODBCS"] {
888 assert_eq!(o.apply(option), Ok(true));
889 }
890 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
891 for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
892 assert_eq!(o.apply(option), Ok(true));
893 }
894 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
895 }
896
897 #[test]
898 fn abbreviations_and_case() {
899 let mut o = Options::default();
900 assert_eq!(o.apply("ar(e)"), Ok(true));
901 assert_eq!(o.apply("CP(1047)"), Ok(true));
902 assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
903 assert_eq!(o.apply("fsrt"), Ok(true));
904 assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
905 assert_eq!(o.apply("NOFASTSRT"), Ok(true));
906 assert!(!o.fastsrt);
907 }
908
909 #[test]
910 fn ibms_table_is_whole_and_no_spelling_names_two_options() {
911 let all: Vec<Documented> = documented().collect();
912 assert_eq!(all.len(), 85, "Table 45 lists 85 options");
913 let mut seen = std::collections::HashMap::new();
914 for o in &all {
915 for s in o.spellings() {
916 assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
917 }
918 }
919 assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
920 let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
921 assert!(!adata.process && adata.page == 345);
922 }
923
924 #[test]
925 fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
926 let suboption = |name| match name {
927 "ARITH" => "(E)",
928 "CODEPAGE" => "(1047)",
929 "TRUNC" => "(OPT)",
930 "NUMPROC" => "(PFD)",
931 "ZONEDATA" => "(MIG)",
932 _ => "",
933 };
934 for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE", "INVDATA", "ZONEDATA", "ZWB"] {
935 let o = documented().find(|o| o.name == name).unwrap();
936 for s in o.spellings() {
937 assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
938 }
939 }
940 let mut o = Options::default();
941 o.apply("FSRT").unwrap();
942 o.apply("NOFSRT").unwrap();
943 assert!(!o.fastsrt);
944 assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
945 assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
946 assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
947 assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
948 }
949
950 #[test]
952 fn the_vendored_table_is_cobolworks() {
953 let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
954 let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
955 assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
956 }
957
958 #[test]
959 fn options_this_layer_does_not_read_pass_through() {
960 assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
961 }
962
963 #[test]
964 fn an_option_that_cannot_be_applied_says_why_and_leaves_the_options_as_they_were() {
965 let mut o = Options::default();
966 o.apply("NUMPROC(PFD)").unwrap();
967 o.apply("TRUNC(BIN)").unwrap();
968 assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
969 assert_eq!(o.apply("CODEPAGE(930)"), Err(OptionError::UnsupportedCodePage(930)));
970 assert!(matches!(o.apply("CP(X)"), Err(OptionError::BadSuboption { .. })));
971 for (option, warns) in [("LIB", false), ("SIZE(MAX)", false), ("sz(2097152)", false), ("FLAGSAA", true), ("NOFDUMP", true)] {
972 assert!(matches!(o.apply(option), Err(OptionError::NoEffect { warning, .. }) if warning == warns), "{option}");
973 }
974 assert_eq!((o.apply("NOLIB"), o.apply("FDUMP")), (Ok(false), Ok(false)));
975 assert_eq!((o.numproc, o.trunc, o.codepage), (Numproc::Pfd, Trunc::Bin, 1140));
976 let removed = o.apply("NUMPROC(MIG)").unwrap_err();
977 assert_eq!(removed.to_string(), "NUMPROC(MIG) was removed in Enterprise COBOL V5, so NUMPROC(NOPFD) is in effect");
978 assert_eq!(o.numproc, Numproc::Nopfd, "the default NUMPROC, not the one before");
979 }
980
981 #[test]
982 fn silent_flag_turns_off_trunc_reports() {
983 let mut o = Options::default();
984 o.apply_flag("-silent").unwrap();
985 assert_eq!(o.trunc_check, TruncCheck::Silent);
986 o.apply_flag("-strict-sort-keys").unwrap();
987 assert_eq!(o.sort_keys, SortKeys::Strict);
988 assert!(o.apply_flag("-quiet").is_err());
989 }
990
991 #[test]
992 fn warnings_proceed_unless_the_flag_blocks_them() {
993 let mut o = Options::default();
994 assert_eq!(o.warnings, Warnings::Proceed);
995 o.apply_flag("-warnings-block").unwrap();
996 assert_eq!(o.warnings, Warnings::Block);
997 assert!(o.apply_flag("-Werror").is_err());
998 }
999
1000 #[test]
1001 fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
1002 let given = |option: &str| {
1003 let mut o = Options::default();
1004 o.apply(option).map(|_| o.compile)
1005 };
1006 assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
1007 assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
1008 assert_eq!(given("c"), Ok(Some(Compile::Full)));
1009 assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
1010 assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
1011 assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
1012 assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
1013 for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
1014 assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1015 }
1016 let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
1017 assert_eq!(codes, [16, 12, 8, 4, 0]);
1018 }
1019
1020 #[test]
1021 fn a_card_outranks_warnings_block() {
1022 let mut o = Options::default();
1023 o.apply_flag("-warnings-block").unwrap();
1024 assert_eq!(o.object_code(), Compile::Until(Stop::W));
1025 o.apply("NOCOMPILE(S)").unwrap();
1026 assert_eq!(o.object_code(), Compile::Until(Stop::S));
1027 o.apply("NOCOMPILE(E)").unwrap();
1028 assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
1029 }
1030
1031 #[test]
1032 fn fastsrt_adv_print_excludes_unless_included() {
1033 let mut o = Options::default();
1034 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
1035 o.apply_flag("--fastsrt-adv-print=include").unwrap();
1036 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
1037 o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
1038 assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
1039 assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
1040 assert!(o.apply_flag("--fastsrt-adv-print").is_err());
1041 }
1042
1043 #[test]
1044 fn the_options_of_roadmap_2_10_to_2_12_default_to_ibms_and_take_each_documented_suboption() {
1045 let o = Options::default();
1046 assert_eq!((o.quote, o.currency, o.nsymbol, o.dispsign, o.intdate), (Quote::Quote, None, Nsymbol::National, DispSign::Compat, IntDate::Ansi));
1047 assert_eq!((o.qualify, o.initial, o.vlr, o.vsamopenfs), (Qualify::Compat, false, Vlr::Standard, VsamOpenFs::Compat));
1048 let given = |cards: &[&str]| {
1049 let mut o = Options::default();
1050 cards.iter().for_each(|c| assert_eq!(o.apply(c), Ok(true), "{c}"));
1051 o
1052 };
1053 assert_eq!(given(&["APOST"]).quote, Quote::Apost);
1054 assert_eq!(given(&["apost", "Q"]).quote, Quote::Quote);
1055 assert_eq!(given(&["NS(DBCS)"]).nsymbol, Nsymbol::Dbcs);
1056 assert_eq!(given(&["NSYMBOL(DBCS)", "NS(NAT)"]).nsymbol, Nsymbol::National);
1057 assert_eq!(given(&["DS(S)"]).dispsign, DispSign::Sep);
1058 assert_eq!(given(&["DISPSIGN(SEP)", "DISPSIGN(COMPAT)"]).dispsign, DispSign::Compat);
1059 assert_eq!(given(&["INTDATE(LILIAN)"]).intdate, IntDate::Lilian);
1060 assert_eq!(given(&["QUA(E)"]).qualify, Qualify::Extend);
1061 assert_eq!(given(&["QUALIFY(EXTEND)", "QUA(C)"]).qualify, Qualify::Compat);
1062 assert!(given(&["INITIAL"]).initial);
1063 assert!(!given(&["INITIAL", "NOINITIAL"]).initial);
1064 assert_eq!(given(&["VLR(C)"]).vlr, Vlr::Compat);
1065 assert_eq!(given(&["VLR(COMPAT)", "VLR(STANDARD)"]).vlr, Vlr::Standard);
1066 assert_eq!(given(&["VS(S)"]).vsamopenfs, VsamOpenFs::Succ);
1067 assert_eq!(given(&["VSAMOPENFS(SUCC)", "VSAMOPENFS(COMPAT)"]).vsamopenfs, VsamOpenFs::Compat);
1068 for bad in ["APOST(X)", "NSYMBOL(N)", "DS(X)", "INTDATE(JULIAN)", "QUA(X)", "INITIAL(Y)", "VLR(X)", "VS(X)", "INTDATE"] {
1069 assert!(matches!(Options::default().apply(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1070 }
1071 }
1072
1073 #[test]
1074 fn currency_takes_one_character_it_may_name_in_either_delimiter_or_a_hexadecimal_byte() {
1075 let currency = |card: &str| {
1076 let mut o = Options::default();
1077 o.apply(card).map(|_| o.currency)
1078 };
1079 assert_eq!(currency("CURRENCY('£')"), Ok(Some(Currency::Char('£'))));
1080 assert_eq!(currency("curr(\"f\")"), Ok(Some(Currency::Char('f'))), "the literal keeps its case");
1081 assert_eq!(currency("CURRENCY(X'5B')"), Ok(Some(Currency::Hex(0x5B))));
1082 assert_eq!(currency("NOCURR"), Ok(None));
1083 for bad in ["CURRENCY('E')", "CURRENCY('e')", "CURRENCY('1')", "CURRENCY(' ')", "CURRENCY('*')", "CURRENCY('EUR')", "CURRENCY(SPACE)", "CURRENCY(N'£')", "CURRENCY(Z'£')", "CURRENCY(X'5B5B')", "CURRENCY", "NOCURRENCY('£')"] {
1084 assert!(matches!(currency(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1085 }
1086 let mut o = Options::default();
1087 o.apply("CURRENCY(X'4A')").unwrap();
1088 assert_eq!(o.currency_symbol(), Some(Ok('¢')), "X'4A' is the cent sign in CCSID 1140");
1089 o.apply("CURRENCY(X'F1')").unwrap();
1090 assert_eq!(o.currency_symbol(), Some(Err('1')));
1091 assert_eq!(Options::default().currency_symbol(), None);
1092 }
1093
1094 #[test]
1095 fn numcheck_takes_ibms_suboption_defaults_and_zonecheck_is_its_zoned_check() {
1096 let numcheck = |card: &str| {
1097 let mut o = Options::default();
1098 o.apply(card).map(|_| o.numcheck)
1099 };
1100 let all = Numcheck::default();
1101 assert_eq!(all, Numcheck { zon: Some(ZonCheck { alphnum: true, lax: false }), pac: true, bin: Some(BinCheck { truncbin: true }), abd: false });
1102 assert_eq!(numcheck("NUMCHECK"), Ok(Some(all)));
1103 assert_eq!(numcheck("NC(ABD)"), Ok(Some(Numcheck { abd: true, ..all })));
1104 assert_eq!(numcheck("NUMCHECK(BIN)"), Ok(Some(Numcheck { zon: None, pac: false, bin: Some(BinCheck { truncbin: true }), abd: false })));
1105 assert_eq!(
1106 numcheck("NUMCHECK(ZON(NOALPHNUM,LAX),NOPAC,BIN(NOTRUNCBIN),ABD)"),
1107 Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: false, lax: true }), pac: false, bin: Some(BinCheck { truncbin: false }), abd: true }))
1108 );
1109 assert_eq!(numcheck("NUMCHECK(ZON(LAXREDEF))"), Ok(Some(Numcheck { zon: Some(ZonCheck { alphnum: true, lax: true }), pac: false, bin: None, abd: false })));
1110 assert_eq!(numcheck("NUMCHECK(NOZON,NOPAC,NOBIN)"), Ok(None));
1111 assert_eq!(numcheck("NONC"), Ok(None));
1112 for bad in ["NUMCHECK(ZON(X))", "NUMCHECK(PAC(X))", "NUMCHECK(BIN(X))", "NUMCHECK(X)", "ZONECHECK", "ZC(X)"] {
1113 assert!(matches!(numcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1114 }
1115 assert_eq!(numcheck("ZC(ABD)"), Ok(Some(Numcheck { zon: Some(ZonCheck::default()), pac: false, bin: None, abd: true })));
1116 let mut o = Options::default();
1117 o.apply("NUMCHECK").unwrap();
1118 o.apply("NOZONECHECK").unwrap();
1119 assert_eq!(o.numcheck, Some(Numcheck { zon: None, ..all }));
1120 o.apply("ZONECHECK(MSG)").unwrap();
1121 o.apply("NOZC").unwrap();
1122 assert_eq!(o.numcheck, None);
1123 }
1124
1125 #[test]
1126 fn parmcheck_and_initcheck_take_their_suboptions_and_ibms_defaults() {
1127 let parmcheck = |card: &str| {
1128 let mut o = Options::default();
1129 o.apply(card).map(|_| o.parmcheck)
1130 };
1131 assert_eq!(parmcheck("PARMCHECK"), Ok(Some(Parmcheck { abd: false, bytes: 100 })));
1132 assert_eq!(parmcheck("PC(ABD)"), Ok(Some(Parmcheck { abd: true, bytes: 100 })));
1133 assert_eq!(parmcheck("PC(5000)"), Ok(Some(Parmcheck { abd: false, bytes: 5000 })));
1134 assert_eq!(parmcheck("PARMCHECK(ABD,1)"), Ok(Some(Parmcheck { abd: true, bytes: 1 })));
1135 assert_eq!(parmcheck("NOPC"), Ok(None));
1136 for bad in ["PC(0)", "PC(10000)", "PC(5000,ABD)", "PC(MSG,1,2)", "PC(X)"] {
1137 assert!(matches!(parmcheck(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
1138 }
1139 let initcheck = |card: &str| {
1140 let mut o = Options::default();
1141 o.apply(card).map(|_| o.initcheck)
1142 };
1143 assert_eq!(initcheck("INITCHECK"), Ok(Some(Initcheck::Lax)));
1144 assert_eq!(initcheck("IC(STRICT)"), Ok(Some(Initcheck::Strict)));
1145 assert_eq!(initcheck("NOIC"), Ok(None));
1146 assert!(matches!(initcheck("IC(X)"), Err(OptionError::BadSuboption { .. })));
1147 let o = Options::default();
1148 assert_eq!((o.numcheck, o.parmcheck, o.initcheck), (None, None, None));
1149 }
1150
1151 #[test]
1152 fn cics_return_warning_is_once_unless_the_flag_says_always_or_never() {
1153 let mut o = Options::default();
1154 assert_eq!(o.cics_return_warning, CicsReturnWarning::Once);
1155 for mode in [CicsReturnWarning::Always, CicsReturnWarning::Never, CicsReturnWarning::Once] {
1156 o.apply_flag(mode.flag()).unwrap();
1157 assert_eq!(o.cics_return_warning, mode);
1158 }
1159 assert!(o.apply_flag("--cics-return-warning=sometimes").is_err());
1160 assert!(o.apply_flag("--cics-return-warning").is_err());
1161 }
1162}