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, Default, PartialEq, Eq)]
157pub enum CicsReturnWarning {
158 #[default]
159 Once,
160 Always,
161 Never,
162}
163
164impl CicsReturnWarning {
165 pub const fn flag(self) -> &'static str {
166 match self {
167 Self::Once => "--cics-return-warning=once",
168 Self::Always => "--cics-return-warning=always",
169 Self::Never => "--cics-return-warning=never",
170 }
171 }
172}
173
174#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
177pub enum Quote {
178 #[default]
179 Quote,
180 Apost,
181}
182
183impl Quote {
184 pub const fn byte(self) -> u8 {
186 match self {
187 Self::Quote => ebcdic::QUOTE,
188 Self::Apost => ebcdic::APOSTROPHE,
189 }
190 }
191
192 pub const fn unit(self) -> u16 {
194 match self {
195 Self::Quote => 0x0022,
196 Self::Apost => 0x0027,
197 }
198 }
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub enum Currency {
205 Char(char),
206 Hex(u8),
207}
208
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
212pub enum Nsymbol {
213 #[default]
214 National,
215 Dbcs,
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
222pub enum DispSign {
223 #[default]
224 Compat,
225 Sep,
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub enum IntDate {
232 #[default]
233 Ansi,
234 Lilian,
235}
236
237#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
240pub enum Qualify {
241 #[default]
242 Compat,
243 Extend,
244}
245
246#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
250pub enum Vlr {
251 #[default]
252 Standard,
253 Compat,
254}
255
256#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
260pub enum VsamOpenFs {
261 #[default]
262 Compat,
263 Succ,
264}
265
266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub enum Warnings {
270 #[default]
271 Proceed,
272 Block,
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum Compile {
279 Full,
281 Until(Stop),
283 SyntaxOnly,
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum Stop {
290 W,
291 E,
292 S,
293}
294
295impl Default for Compile {
296 fn default() -> Self {
297 Self::Until(Stop::S)
298 }
299}
300
301impl Compile {
302 pub const fn stops_at(self) -> u8 {
305 match self {
306 Self::Full => 16,
307 Self::Until(Stop::S) => 12,
308 Self::Until(Stop::E) => 8,
309 Self::Until(Stop::W) => 4,
310 Self::SyntaxOnly => 0,
311 }
312 }
313}
314
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub struct Options {
317 pub arith: Arith,
318 pub trunc: Trunc,
319 pub numproc: Numproc,
320 pub codepage: u16,
321 pub trunc_check: TruncCheck,
322 pub fastsrt: bool,
324 pub fastsrt_adv_print: FastsrtAdvPrint,
325 pub sort_keys: SortKeys,
326 pub adv: bool,
329 pub thread: bool,
331 pub dll: bool,
332 pub rent: bool,
333 pub dbcs: bool,
334 pub warnings: Warnings,
335 pub compile: Option<Compile>,
337 pub dynam: bool,
339 pub debug: bool,
342 pub cics_return_warning: CicsReturnWarning,
343 pub invdata: Option<Invdata>,
345 pub zwb: bool,
347 pub quote: Quote,
348 pub currency: Option<Currency>,
351 pub nsymbol: Nsymbol,
352 pub dispsign: DispSign,
353 pub intdate: IntDate,
354 pub qualify: Qualify,
355 pub initial: bool,
359 pub vlr: Vlr,
360 pub vsamopenfs: VsamOpenFs,
361}
362
363impl Default for Options {
364 fn default() -> Self {
365 Self {
366 arith: Arith::default(),
367 trunc: Trunc::default(),
368 numproc: Numproc::default(),
369 codepage: 1140,
370 trunc_check: TruncCheck::default(),
371 fastsrt: false,
372 fastsrt_adv_print: FastsrtAdvPrint::default(),
373 sort_keys: SortKeys::default(),
374 adv: true,
375 thread: false,
376 dll: false,
377 rent: true,
378 dbcs: true,
379 warnings: Warnings::default(),
380 compile: None,
381 dynam: false,
382 debug: false,
383 cics_return_warning: CicsReturnWarning::default(),
384 invdata: None,
385 zwb: true,
386 quote: Quote::default(),
387 currency: None,
388 nsymbol: Nsymbol::default(),
389 dispsign: DispSign::default(),
390 intdate: IntDate::default(),
391 qualify: Qualify::default(),
392 initial: false,
393 vlr: Vlr::default(),
394 vsamopenfs: VsamOpenFs::default(),
395 }
396 }
397}
398
399#[derive(Clone, Debug, PartialEq, Eq)]
402pub enum OptionError {
403 BadSuboption { option: String, given: String },
405 Removed { option: String, since: &'static str, instead: &'static str },
407 NoEffect { option: &'static str, why: &'static str, warning: bool },
410 UnsupportedCodePage(u16),
411 UnknownFlag(String),
412}
413
414impl fmt::Display for OptionError {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 match self {
417 Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
418 Self::Removed { option, since, instead } => write!(f, "{option} was removed in Enterprise COBOL {since}, so {instead} is in effect"),
419 Self::NoEffect { option, why, .. } => write!(f, "{option} {why}"),
420 Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not a single-byte EBCDIC page this compiler carries"),
421 Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
422 }
423 }
424}
425
426fn without_effect(name: &str) -> Option<OptionError> {
430 let (option, why, warning) = match name {
431 "LIB" => ("LIB", "is no longer needed: COPY members are always read from the libraries", false),
432 "SIZE" | "SZ" => ("SIZE", "was removed in Enterprise COBOL V5 and has no effect", false),
433 "FLAGSAA" => ("FLAGSAA", "is not an Enterprise COBOL option and has no effect", true),
434 "NOFDUMP" => ("NOFDUMP", "is not an Enterprise COBOL option and has no effect", true),
435 _ => return None,
436 };
437 Some(OptionError::NoEffect { option, why, warning })
438}
439
440fn currency_literal(text: &str) -> Option<Currency> {
444 fn quoted(t: &str) -> Option<&str> {
445 let q = t.chars().next().filter(|q| matches!(q, '\'' | '"'))?;
446 (t.len() >= 2 && t.ends_with(q)).then(|| &t[1..t.len() - 1])
447 }
448 let text = text.trim();
449 if let Some(hex) = text.strip_prefix(['X', 'x']).and_then(quoted) {
450 return u8::from_str_radix(hex, 16).ok().filter(|_| hex.len() == 2).map(Currency::Hex);
451 }
452 let mut chars = quoted(text)?.chars();
453 match (chars.next(), chars.next()) {
454 (Some(c), None) if can_be_currency(c) => Some(Currency::Char(c)),
455 _ => None,
456 }
457}
458
459pub fn can_be_currency(c: char) -> bool {
462 u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz *+-/,.;()\"=".contains(c)
463}
464
465impl std::error::Error for OptionError {}
466
467impl Options {
468 pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
472 let given = option.trim();
473 let option = given.to_ascii_uppercase();
474 let (name, sub) = match option.split_once('(') {
475 Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
476 None => (option.as_str(), ""),
477 };
478 let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
479 if let Some(e) = without_effect(name) {
480 return Err(e);
481 }
482 let Some((documented, off)) = spelled(name) else { return Ok(false) };
483 match documented.name {
484 "ARITH" => {
485 self.arith = match sub {
486 "COMPAT" | "C" => Arith::Compat,
487 "EXTEND" | "E" => Arith::Extend,
488 _ => return Err(bad()),
489 }
490 }
491 "TRUNC" => {
492 self.trunc = match sub {
493 "STD" => Trunc::Std,
494 "OPT" => Trunc::Opt,
495 "BIN" => Trunc::Bin,
496 _ => return Err(bad()),
497 }
498 }
499 "NUMPROC" => {
500 self.numproc = match sub {
501 "NOPFD" => Numproc::Nopfd,
502 "PFD" => Numproc::Pfd,
503 "MIG" => {
504 self.numproc = Numproc::default();
505 return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5", instead: "NUMPROC(NOPFD)" });
506 }
507 _ => return Err(bad()),
508 }
509 }
510 "CODEPAGE" => {
511 let ccsid: u16 = sub.parse().map_err(|_| bad())?;
512 CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
513 self.codepage = ccsid;
514 }
515 "COMPILE" => {
516 self.compile = Some(match (off, sub) {
517 (false, "") => Compile::Full,
518 (true, "") => Compile::SyntaxOnly,
519 (true, "W") => Compile::Until(Stop::W),
520 (true, "E") => Compile::Until(Stop::E),
521 (true, "S") => Compile::Until(Stop::S),
522 _ => return Err(bad()),
523 })
524 }
525 "INVDATA" if off => self.invdata = None,
526 "INVDATA" => {
527 let mut invdata = Invdata::default();
528 for part in sub.split(',').map(str::trim).filter(|p| !p.is_empty()) {
529 match part {
530 "FORCENUMCMP" | "FNC" => invdata.forcenumcmp = true,
531 "NOFORCENUMCMP" | "NOFNC" => invdata.forcenumcmp = false,
532 "CLEANSIGN" | "CS" => invdata.cleansign = true,
533 "NOCLEANSIGN" | "NOCS" => invdata.cleansign = false,
534 _ => return Err(bad()),
535 }
536 }
537 self.invdata = Some(invdata);
538 }
539 "ZONEDATA" => {
540 self.invdata = match sub {
541 "PFD" => None,
542 "NOPFD" => Some(Invdata::default()),
543 "MIG" => Some(Invdata { forcenumcmp: true, cleansign: true }),
544 _ => return Err(bad()),
545 }
546 }
547 "ZWB" => self.zwb = !off,
548 "FASTSRT" => self.fastsrt = !off,
549 "ADV" => self.adv = !off,
550 "THREAD" => self.thread = !off,
551 "DLL" => self.dll = !off,
552 "RENT" => self.rent = !off,
553 "DBCS" => self.dbcs = !off,
554 "DYNAM" => self.dynam = !off,
555 "APOST/QUOTE" if sub.is_empty() => self.quote = if name == "APOST" { Quote::Apost } else { Quote::Quote },
556 "CURRENCY" => {
557 self.currency = match (off, sub) {
558 (true, "") => None,
559 (false, _) => Some(currency_literal(given.split_once('(').map_or("", |(_, rest)| rest.trim_end().trim_end_matches(')'))).ok_or_else(bad)?),
560 _ => return Err(bad()),
561 }
562 }
563 "NSYMBOL" => {
564 self.nsymbol = match sub {
565 "NATIONAL" | "NAT" => Nsymbol::National,
566 "DBCS" => Nsymbol::Dbcs,
567 _ => return Err(bad()),
568 }
569 }
570 "DISPSIGN" => {
571 self.dispsign = match sub {
572 "COMPAT" | "C" => DispSign::Compat,
573 "SEP" | "S" => DispSign::Sep,
574 _ => return Err(bad()),
575 }
576 }
577 "INTDATE" => {
578 self.intdate = match sub {
579 "ANSI" => IntDate::Ansi,
580 "LILIAN" => IntDate::Lilian,
581 _ => return Err(bad()),
582 }
583 }
584 "QUALIFY" => {
585 self.qualify = match sub {
586 "COMPAT" | "C" => Qualify::Compat,
587 "EXTEND" | "E" => Qualify::Extend,
588 _ => return Err(bad()),
589 }
590 }
591 "INITIAL" if sub.is_empty() => self.initial = !off,
592 "VLR" => {
593 self.vlr = match sub {
594 "STANDARD" | "S" => Vlr::Standard,
595 "COMPAT" | "C" => Vlr::Compat,
596 _ => return Err(bad()),
597 }
598 }
599 "VSAMOPENFS" => {
600 self.vsamopenfs = match sub {
601 "COMPAT" | "C" => VsamOpenFs::Compat,
602 "SUCC" | "S" => VsamOpenFs::Succ,
603 _ => return Err(bad()),
604 }
605 }
606 "APOST/QUOTE" | "INITIAL" => return Err(bad()),
607 _ => return Ok(false),
608 }
609 Ok(true)
610 }
611
612 pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
614 match flag {
615 "-silent" => self.trunc_check = TruncCheck::Silent,
616 "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
617 "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
618 "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
619 "-warnings-block" => self.warnings = Warnings::Block,
620 "-debug" => self.debug = true,
621 "--cics-return-warning=once" => self.cics_return_warning = CicsReturnWarning::Once,
622 "--cics-return-warning=always" => self.cics_return_warning = CicsReturnWarning::Always,
623 "--cics-return-warning=never" => self.cics_return_warning = CicsReturnWarning::Never,
624 _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
625 }
626 Ok(())
627 }
628
629 pub fn object_code(&self) -> Compile {
633 self.compile.unwrap_or(match self.warnings {
634 Warnings::Block => Compile::Until(Stop::W),
635 Warnings::Proceed => Compile::default(),
636 })
637 }
638
639 pub fn code_page(&self) -> &'static CodePage {
640 CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
641 }
642
643 pub fn currency_symbol(&self) -> Option<Result<char, char>> {
646 self.currency.map(|c| match c {
647 Currency::Char(c) => Ok(c),
648 Currency::Hex(b) => {
649 let c = self.code_page().decode_byte(b);
650 if can_be_currency(c) { Ok(c) } else { Err(c) }
651 }
652 })
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 #[test]
661 fn defaults_are_ibms() {
662 let o = Options::default();
663 assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
664 assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
665 }
666
667 #[test]
668 fn invdata_zonedata_and_zwb() {
669 let mut o = Options::default();
670 assert_eq!((o.invdata, o.zwb), (None, true));
671 assert_eq!(o.apply("INVDATA"), Ok(true));
672 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: false, cleansign: true }));
673 assert_eq!(o.apply("INVD(FNC,NOCS)"), Ok(true));
674 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: false }));
675 assert_eq!(o.apply("NOINVDATA"), Ok(true));
676 assert_eq!(o.invdata, None);
677 assert_eq!(o.apply("ZD(MIG)"), Ok(true));
678 assert_eq!(o.invdata, Some(Invdata { forcenumcmp: true, cleansign: true }));
679 assert_eq!(o.apply("ZONEDATA(NOPFD)"), Ok(true));
680 assert_eq!(o.invdata, Some(Invdata::default()));
681 assert_eq!(o.apply("ZONEDATA(PFD)"), Ok(true));
682 assert_eq!(o.invdata, None);
683 assert!(o.apply("INVDATA(SOMETIMES)").is_err());
684 assert_eq!(o.apply("NOZWB"), Ok(true));
685 assert!(!o.zwb);
686 }
687
688 #[test]
689 fn dynam_and_its_abbreviations() {
690 let mut o = Options::default();
691 assert_eq!(o.apply("DYN"), Ok(true));
692 assert!(o.dynam);
693 assert_eq!(o.apply("NODYNAM"), Ok(true));
694 assert!(!o.dynam);
695 }
696
697 #[test]
698 fn adv_and_noadv_have_no_abbreviations() {
699 let mut o = Options::default();
700 assert_eq!(o.apply("noadv"), Ok(true));
701 assert!(!o.adv);
702 assert_eq!(o.apply("ADV"), Ok(true));
703 assert!(o.adv);
704 assert_eq!(o.apply("NOAD"), Ok(false));
705 }
706
707 #[test]
708 fn thread_dll_rent_and_dbcs_and_their_negatives() {
709 let mut o = Options::default();
710 for option in ["thread", "DLL", "NORENT", "NODBCS"] {
711 assert_eq!(o.apply(option), Ok(true));
712 }
713 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
714 for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
715 assert_eq!(o.apply(option), Ok(true));
716 }
717 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
718 }
719
720 #[test]
721 fn abbreviations_and_case() {
722 let mut o = Options::default();
723 assert_eq!(o.apply("ar(e)"), Ok(true));
724 assert_eq!(o.apply("CP(1047)"), Ok(true));
725 assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
726 assert_eq!(o.apply("fsrt"), Ok(true));
727 assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
728 assert_eq!(o.apply("NOFASTSRT"), Ok(true));
729 assert!(!o.fastsrt);
730 }
731
732 #[test]
733 fn ibms_table_is_whole_and_no_spelling_names_two_options() {
734 let all: Vec<Documented> = documented().collect();
735 assert_eq!(all.len(), 85, "Table 45 lists 85 options");
736 let mut seen = std::collections::HashMap::new();
737 for o in &all {
738 for s in o.spellings() {
739 assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
740 }
741 }
742 assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
743 let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
744 assert!(!adata.process && adata.page == 345);
745 }
746
747 #[test]
748 fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
749 let suboption = |name| match name {
750 "ARITH" => "(E)",
751 "CODEPAGE" => "(1047)",
752 "TRUNC" => "(OPT)",
753 "NUMPROC" => "(PFD)",
754 "ZONEDATA" => "(MIG)",
755 _ => "",
756 };
757 for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE", "INVDATA", "ZONEDATA", "ZWB"] {
758 let o = documented().find(|o| o.name == name).unwrap();
759 for s in o.spellings() {
760 assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
761 }
762 }
763 let mut o = Options::default();
764 o.apply("FSRT").unwrap();
765 o.apply("NOFSRT").unwrap();
766 assert!(!o.fastsrt);
767 assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
768 assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
769 assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
770 assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
771 }
772
773 #[test]
775 fn the_vendored_table_is_cobolworks() {
776 let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
777 let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
778 assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
779 }
780
781 #[test]
782 fn options_this_layer_does_not_read_pass_through() {
783 assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
784 }
785
786 #[test]
787 fn an_option_that_cannot_be_applied_says_why_and_leaves_the_options_as_they_were() {
788 let mut o = Options::default();
789 o.apply("NUMPROC(PFD)").unwrap();
790 o.apply("TRUNC(BIN)").unwrap();
791 assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
792 assert_eq!(o.apply("CODEPAGE(930)"), Err(OptionError::UnsupportedCodePage(930)));
793 assert!(matches!(o.apply("CP(X)"), Err(OptionError::BadSuboption { .. })));
794 for (option, warns) in [("LIB", false), ("SIZE(MAX)", false), ("sz(2097152)", false), ("FLAGSAA", true), ("NOFDUMP", true)] {
795 assert!(matches!(o.apply(option), Err(OptionError::NoEffect { warning, .. }) if warning == warns), "{option}");
796 }
797 assert_eq!((o.apply("NOLIB"), o.apply("FDUMP")), (Ok(false), Ok(false)));
798 assert_eq!((o.numproc, o.trunc, o.codepage), (Numproc::Pfd, Trunc::Bin, 1140));
799 let removed = o.apply("NUMPROC(MIG)").unwrap_err();
800 assert_eq!(removed.to_string(), "NUMPROC(MIG) was removed in Enterprise COBOL V5, so NUMPROC(NOPFD) is in effect");
801 assert_eq!(o.numproc, Numproc::Nopfd, "the default NUMPROC, not the one before");
802 }
803
804 #[test]
805 fn silent_flag_turns_off_trunc_reports() {
806 let mut o = Options::default();
807 o.apply_flag("-silent").unwrap();
808 assert_eq!(o.trunc_check, TruncCheck::Silent);
809 o.apply_flag("-strict-sort-keys").unwrap();
810 assert_eq!(o.sort_keys, SortKeys::Strict);
811 assert!(o.apply_flag("-quiet").is_err());
812 }
813
814 #[test]
815 fn warnings_proceed_unless_the_flag_blocks_them() {
816 let mut o = Options::default();
817 assert_eq!(o.warnings, Warnings::Proceed);
818 o.apply_flag("-warnings-block").unwrap();
819 assert_eq!(o.warnings, Warnings::Block);
820 assert!(o.apply_flag("-Werror").is_err());
821 }
822
823 #[test]
824 fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
825 let given = |option: &str| {
826 let mut o = Options::default();
827 o.apply(option).map(|_| o.compile)
828 };
829 assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
830 assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
831 assert_eq!(given("c"), Ok(Some(Compile::Full)));
832 assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
833 assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
834 assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
835 assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
836 for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
837 assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
838 }
839 let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
840 assert_eq!(codes, [16, 12, 8, 4, 0]);
841 }
842
843 #[test]
844 fn a_card_outranks_warnings_block() {
845 let mut o = Options::default();
846 o.apply_flag("-warnings-block").unwrap();
847 assert_eq!(o.object_code(), Compile::Until(Stop::W));
848 o.apply("NOCOMPILE(S)").unwrap();
849 assert_eq!(o.object_code(), Compile::Until(Stop::S));
850 o.apply("NOCOMPILE(E)").unwrap();
851 assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
852 }
853
854 #[test]
855 fn fastsrt_adv_print_excludes_unless_included() {
856 let mut o = Options::default();
857 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
858 o.apply_flag("--fastsrt-adv-print=include").unwrap();
859 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
860 o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
861 assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
862 assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
863 assert!(o.apply_flag("--fastsrt-adv-print").is_err());
864 }
865
866 #[test]
867 fn the_options_of_roadmap_2_10_to_2_12_default_to_ibms_and_take_each_documented_suboption() {
868 let o = Options::default();
869 assert_eq!((o.quote, o.currency, o.nsymbol, o.dispsign, o.intdate), (Quote::Quote, None, Nsymbol::National, DispSign::Compat, IntDate::Ansi));
870 assert_eq!((o.qualify, o.initial, o.vlr, o.vsamopenfs), (Qualify::Compat, false, Vlr::Standard, VsamOpenFs::Compat));
871 let given = |cards: &[&str]| {
872 let mut o = Options::default();
873 cards.iter().for_each(|c| assert_eq!(o.apply(c), Ok(true), "{c}"));
874 o
875 };
876 assert_eq!(given(&["APOST"]).quote, Quote::Apost);
877 assert_eq!(given(&["apost", "Q"]).quote, Quote::Quote);
878 assert_eq!(given(&["NS(DBCS)"]).nsymbol, Nsymbol::Dbcs);
879 assert_eq!(given(&["NSYMBOL(DBCS)", "NS(NAT)"]).nsymbol, Nsymbol::National);
880 assert_eq!(given(&["DS(S)"]).dispsign, DispSign::Sep);
881 assert_eq!(given(&["DISPSIGN(SEP)", "DISPSIGN(COMPAT)"]).dispsign, DispSign::Compat);
882 assert_eq!(given(&["INTDATE(LILIAN)"]).intdate, IntDate::Lilian);
883 assert_eq!(given(&["QUA(E)"]).qualify, Qualify::Extend);
884 assert_eq!(given(&["QUALIFY(EXTEND)", "QUA(C)"]).qualify, Qualify::Compat);
885 assert!(given(&["INITIAL"]).initial);
886 assert!(!given(&["INITIAL", "NOINITIAL"]).initial);
887 assert_eq!(given(&["VLR(C)"]).vlr, Vlr::Compat);
888 assert_eq!(given(&["VLR(COMPAT)", "VLR(STANDARD)"]).vlr, Vlr::Standard);
889 assert_eq!(given(&["VS(S)"]).vsamopenfs, VsamOpenFs::Succ);
890 assert_eq!(given(&["VSAMOPENFS(SUCC)", "VSAMOPENFS(COMPAT)"]).vsamopenfs, VsamOpenFs::Compat);
891 for bad in ["APOST(X)", "NSYMBOL(N)", "DS(X)", "INTDATE(JULIAN)", "QUA(X)", "INITIAL(Y)", "VLR(X)", "VS(X)", "INTDATE"] {
892 assert!(matches!(Options::default().apply(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
893 }
894 }
895
896 #[test]
897 fn currency_takes_one_character_it_may_name_in_either_delimiter_or_a_hexadecimal_byte() {
898 let currency = |card: &str| {
899 let mut o = Options::default();
900 o.apply(card).map(|_| o.currency)
901 };
902 assert_eq!(currency("CURRENCY('£')"), Ok(Some(Currency::Char('£'))));
903 assert_eq!(currency("curr(\"f\")"), Ok(Some(Currency::Char('f'))), "the literal keeps its case");
904 assert_eq!(currency("CURRENCY(X'5B')"), Ok(Some(Currency::Hex(0x5B))));
905 assert_eq!(currency("NOCURR"), Ok(None));
906 for bad in ["CURRENCY('E')", "CURRENCY('e')", "CURRENCY('1')", "CURRENCY(' ')", "CURRENCY('*')", "CURRENCY('EUR')", "CURRENCY(SPACE)", "CURRENCY(N'£')", "CURRENCY(Z'£')", "CURRENCY(X'5B5B')", "CURRENCY", "NOCURRENCY('£')"] {
907 assert!(matches!(currency(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
908 }
909 let mut o = Options::default();
910 o.apply("CURRENCY(X'4A')").unwrap();
911 assert_eq!(o.currency_symbol(), Some(Ok('¢')), "X'4A' is the cent sign in CCSID 1140");
912 o.apply("CURRENCY(X'F1')").unwrap();
913 assert_eq!(o.currency_symbol(), Some(Err('1')));
914 assert_eq!(Options::default().currency_symbol(), None);
915 }
916
917 #[test]
918 fn cics_return_warning_is_once_unless_the_flag_says_always_or_never() {
919 let mut o = Options::default();
920 assert_eq!(o.cics_return_warning, CicsReturnWarning::Once);
921 for mode in [CicsReturnWarning::Always, CicsReturnWarning::Never, CicsReturnWarning::Once] {
922 o.apply_flag(mode.flag()).unwrap();
923 assert_eq!(o.cics_return_warning, mode);
924 }
925 assert!(o.apply_flag("--cics-return-warning=sometimes").is_err());
926 assert!(o.apply_flag("--cics-return-warning").is_err());
927 }
928}