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