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