1use std::fmt;
2use zarch::ebcdic::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, Default, PartialEq, Eq)]
140pub enum Warnings {
141 #[default]
142 Proceed,
143 Block,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum Compile {
150 Full,
152 Until(Stop),
154 SyntaxOnly,
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub enum Stop {
161 W,
162 E,
163 S,
164}
165
166impl Default for Compile {
167 fn default() -> Self {
168 Self::Until(Stop::S)
169 }
170}
171
172impl Compile {
173 pub const fn stops_at(self) -> u8 {
176 match self {
177 Self::Full => 16,
178 Self::Until(Stop::S) => 12,
179 Self::Until(Stop::E) => 8,
180 Self::Until(Stop::W) => 4,
181 Self::SyntaxOnly => 0,
182 }
183 }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct Options {
188 pub arith: Arith,
189 pub trunc: Trunc,
190 pub numproc: Numproc,
191 pub codepage: u16,
192 pub trunc_check: TruncCheck,
193 pub fastsrt: bool,
195 pub fastsrt_adv_print: FastsrtAdvPrint,
196 pub sort_keys: SortKeys,
197 pub adv: bool,
200 pub thread: bool,
202 pub dll: bool,
203 pub rent: bool,
204 pub dbcs: bool,
205 pub warnings: Warnings,
206 pub compile: Option<Compile>,
208 pub dynam: bool,
210 pub debug: bool,
213}
214
215impl Default for Options {
216 fn default() -> Self {
217 Self {
218 arith: Arith::default(),
219 trunc: Trunc::default(),
220 numproc: Numproc::default(),
221 codepage: 1140,
222 trunc_check: TruncCheck::default(),
223 fastsrt: false,
224 fastsrt_adv_print: FastsrtAdvPrint::default(),
225 sort_keys: SortKeys::default(),
226 adv: true,
227 thread: false,
228 dll: false,
229 rent: true,
230 dbcs: true,
231 warnings: Warnings::default(),
232 compile: None,
233 dynam: false,
234 debug: false,
235 }
236 }
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub enum OptionError {
241 BadSuboption { option: String, given: String },
242 Removed { option: String, since: &'static str },
243 UnsupportedCodePage(u16),
244 UnknownFlag(String),
245}
246
247impl fmt::Display for OptionError {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 Self::BadSuboption { option, given } => write!(f, "{option} does not take ({given})"),
251 Self::Removed { option, since } => write!(f, "{option} was removed in Enterprise COBOL {since}"),
252 Self::UnsupportedCodePage(ccsid) => write!(f, "CODEPAGE({ccsid}) is not a single-byte EBCDIC page this compiler carries"),
253 Self::UnknownFlag(flag) => write!(f, "unknown flag {flag}"),
254 }
255 }
256}
257
258impl std::error::Error for OptionError {}
259
260impl Options {
261 pub fn apply(&mut self, option: &str) -> Result<bool, OptionError> {
264 let option = option.trim().to_ascii_uppercase();
265 let (name, sub) = match option.split_once('(') {
266 Some((name, rest)) => (name.trim(), rest.trim_end_matches(')').trim()),
267 None => (option.as_str(), ""),
268 };
269 let bad = || OptionError::BadSuboption { option: name.to_owned(), given: sub.to_owned() };
270 let Some((documented, off)) = spelled(name) else { return Ok(false) };
271 match documented.name {
272 "ARITH" => {
273 self.arith = match sub {
274 "COMPAT" | "C" => Arith::Compat,
275 "EXTEND" | "E" => Arith::Extend,
276 _ => return Err(bad()),
277 }
278 }
279 "TRUNC" => {
280 self.trunc = match sub {
281 "STD" => Trunc::Std,
282 "OPT" => Trunc::Opt,
283 "BIN" => Trunc::Bin,
284 _ => return Err(bad()),
285 }
286 }
287 "NUMPROC" => {
288 self.numproc = match sub {
289 "NOPFD" => Numproc::Nopfd,
290 "PFD" => Numproc::Pfd,
291 "MIG" => return Err(OptionError::Removed { option: "NUMPROC(MIG)".into(), since: "V5" }),
292 _ => return Err(bad()),
293 }
294 }
295 "CODEPAGE" => {
296 let ccsid: u16 = sub.parse().map_err(|_| bad())?;
297 CodePage::by_ccsid(ccsid).ok_or(OptionError::UnsupportedCodePage(ccsid))?;
298 self.codepage = ccsid;
299 }
300 "COMPILE" => {
301 self.compile = Some(match (off, sub) {
302 (false, "") => Compile::Full,
303 (true, "") => Compile::SyntaxOnly,
304 (true, "W") => Compile::Until(Stop::W),
305 (true, "E") => Compile::Until(Stop::E),
306 (true, "S") => Compile::Until(Stop::S),
307 _ => return Err(bad()),
308 })
309 }
310 "FASTSRT" => self.fastsrt = !off,
311 "ADV" => self.adv = !off,
312 "THREAD" => self.thread = !off,
313 "DLL" => self.dll = !off,
314 "RENT" => self.rent = !off,
315 "DBCS" => self.dbcs = !off,
316 "DYNAM" => self.dynam = !off,
317 _ => return Ok(false),
318 }
319 Ok(true)
320 }
321
322 pub fn apply_flag(&mut self, flag: &str) -> Result<(), OptionError> {
324 match flag {
325 "-silent" => self.trunc_check = TruncCheck::Silent,
326 "-strict-sort-keys" => self.sort_keys = SortKeys::Strict,
327 "--fastsrt-adv-print=exclude" => self.fastsrt_adv_print = FastsrtAdvPrint::Exclude,
328 "--fastsrt-adv-print=include" => self.fastsrt_adv_print = FastsrtAdvPrint::Include,
329 "-warnings-block" => self.warnings = Warnings::Block,
330 "-debug" => self.debug = true,
331 _ => return Err(OptionError::UnknownFlag(flag.to_owned())),
332 }
333 Ok(())
334 }
335
336 pub fn object_code(&self) -> Compile {
340 self.compile.unwrap_or(match self.warnings {
341 Warnings::Block => Compile::Until(Stop::W),
342 Warnings::Proceed => Compile::default(),
343 })
344 }
345
346 pub fn code_page(&self) -> &'static CodePage {
347 CodePage::by_ccsid(self.codepage).expect("codepage validated when applied")
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn defaults_are_ibms() {
357 let o = Options::default();
358 assert_eq!((o.arith, o.trunc, o.numproc, o.codepage, o.fastsrt, o.adv), (Arith::Compat, Trunc::Std, Numproc::Nopfd, 1140, false, true));
359 assert_eq!((o.thread, o.dll, o.rent, o.dbcs, o.dynam), (false, false, true, true, false));
360 }
361
362 #[test]
363 fn dynam_and_its_abbreviations() {
364 let mut o = Options::default();
365 assert_eq!(o.apply("DYN"), Ok(true));
366 assert!(o.dynam);
367 assert_eq!(o.apply("NODYNAM"), Ok(true));
368 assert!(!o.dynam);
369 }
370
371 #[test]
372 fn adv_and_noadv_have_no_abbreviations() {
373 let mut o = Options::default();
374 assert_eq!(o.apply("noadv"), Ok(true));
375 assert!(!o.adv);
376 assert_eq!(o.apply("ADV"), Ok(true));
377 assert!(o.adv);
378 assert_eq!(o.apply("NOAD"), Ok(false));
379 }
380
381 #[test]
382 fn thread_dll_rent_and_dbcs_and_their_negatives() {
383 let mut o = Options::default();
384 for option in ["thread", "DLL", "NORENT", "NODBCS"] {
385 assert_eq!(o.apply(option), Ok(true));
386 }
387 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (true, true, false, false));
388 for option in ["NOTHREAD", "NODLL", "RENT", "DBCS"] {
389 assert_eq!(o.apply(option), Ok(true));
390 }
391 assert_eq!((o.thread, o.dll, o.rent, o.dbcs), (false, false, true, true));
392 }
393
394 #[test]
395 fn abbreviations_and_case() {
396 let mut o = Options::default();
397 assert_eq!(o.apply("ar(e)"), Ok(true));
398 assert_eq!(o.apply("CP(1047)"), Ok(true));
399 assert_eq!(o.apply("TRUNC(BIN)"), Ok(true));
400 assert_eq!(o.apply("fsrt"), Ok(true));
401 assert_eq!((o.arith, o.codepage, o.trunc, o.fastsrt), (Arith::Extend, 1047, Trunc::Bin, true));
402 assert_eq!(o.apply("NOFASTSRT"), Ok(true));
403 assert!(!o.fastsrt);
404 }
405
406 #[test]
407 fn ibms_table_is_whole_and_no_spelling_names_two_options() {
408 let all: Vec<Documented> = documented().collect();
409 assert_eq!(all.len(), 85, "Table 45 lists 85 options");
410 let mut seen = std::collections::HashMap::new();
411 for o in &all {
412 for s in o.spellings() {
413 assert!(seen.insert(s, o.name).is_none(), "{s} names {} and {}", seen[s], o.name);
414 }
415 }
416 assert!(TABLE.lines().nth(1).is_some_and(|l| l.contains("cobolwork's provenance/enterprise-options.json")));
417 let adata = all.iter().find(|o| o.name == "ADATA").unwrap();
418 assert!(!adata.process && adata.page == 345);
419 }
420
421 #[test]
422 fn every_spelling_ibm_documents_for_an_option_read_here_is_read() {
423 let suboption = |name| match name {
424 "ARITH" => "(E)",
425 "CODEPAGE" => "(1047)",
426 "TRUNC" => "(OPT)",
427 "NUMPROC" => "(PFD)",
428 _ => "",
429 };
430 for name in ["ARITH", "CODEPAGE", "TRUNC", "NUMPROC", "FASTSRT", "COMPILE"] {
431 let o = documented().find(|o| o.name == name).unwrap();
432 for s in o.spellings() {
433 assert_eq!(Options::default().apply(&format!("{s}{}", suboption(name))), Ok(true), "{s}");
434 }
435 }
436 let mut o = Options::default();
437 o.apply("FSRT").unwrap();
438 o.apply("NOFSRT").unwrap();
439 assert!(!o.fastsrt);
440 assert_eq!(switch("ssr(zlen)", "SSRANGE"), Some(true));
441 assert_eq!(switch("SSRANGE(NOZLEN,MSG)", "SSRANGE"), Some(true));
442 assert_eq!(switch("NOSSR", "SSRANGE"), Some(false));
443 assert_eq!(switch("TRUNC(OPT)", "SSRANGE"), None);
444 }
445
446 #[test]
448 fn the_vendored_table_is_cobolworks() {
449 let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
450 let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join("provenance/enterprise-options.tsv")).expect("cobolwork's table");
451 assert!(theirs == TABLE, "crates/numeric/data/enterprise-options.tsv differs from cobolwork's: run tools/sync-option-table.sh");
452 }
453
454 #[test]
455 fn options_this_layer_does_not_read_pass_through() {
456 assert_eq!(Options::default().apply("SSRANGE"), Ok(false));
457 }
458
459 #[test]
460 fn refusals_name_the_problem() {
461 let mut o = Options::default();
462 assert!(matches!(o.apply("NUMPROC(MIG)"), Err(OptionError::Removed { .. })));
463 assert!(matches!(o.apply("TRUNC(FAST)"), Err(OptionError::BadSuboption { .. })));
464 assert_eq!(o.apply("CODEPAGE(930)"), Err(OptionError::UnsupportedCodePage(930)));
465 }
466
467 #[test]
468 fn silent_flag_turns_off_trunc_reports() {
469 let mut o = Options::default();
470 o.apply_flag("-silent").unwrap();
471 assert_eq!(o.trunc_check, TruncCheck::Silent);
472 o.apply_flag("-strict-sort-keys").unwrap();
473 assert_eq!(o.sort_keys, SortKeys::Strict);
474 assert!(o.apply_flag("-quiet").is_err());
475 }
476
477 #[test]
478 fn warnings_proceed_unless_the_flag_blocks_them() {
479 let mut o = Options::default();
480 assert_eq!(o.warnings, Warnings::Proceed);
481 o.apply_flag("-warnings-block").unwrap();
482 assert_eq!(o.warnings, Warnings::Block);
483 assert!(o.apply_flag("-Werror").is_err());
484 }
485
486 #[test]
487 fn compile_and_nocompile_with_each_severity_and_their_abbreviations() {
488 let given = |option: &str| {
489 let mut o = Options::default();
490 o.apply(option).map(|_| o.compile)
491 };
492 assert_eq!(Options::default().object_code(), Compile::Until(Stop::S));
493 assert_eq!(given("COMPILE"), Ok(Some(Compile::Full)));
494 assert_eq!(given("c"), Ok(Some(Compile::Full)));
495 assert_eq!(given("NOCOMPILE"), Ok(Some(Compile::SyntaxOnly)));
496 assert_eq!(given("NOC(w)"), Ok(Some(Compile::Until(Stop::W))));
497 assert_eq!(given("NOCOMPILE(E)"), Ok(Some(Compile::Until(Stop::E))));
498 assert_eq!(given("NOC(S)"), Ok(Some(Compile::Until(Stop::S))));
499 for bad in ["NOCOMPILE(U)", "NOC(I)", "COMPILE(S)", "C(E)"] {
500 assert!(matches!(given(bad), Err(OptionError::BadSuboption { .. })), "{bad}");
501 }
502 let codes = [Compile::Full, Compile::Until(Stop::S), Compile::Until(Stop::E), Compile::Until(Stop::W), Compile::SyntaxOnly].map(Compile::stops_at);
503 assert_eq!(codes, [16, 12, 8, 4, 0]);
504 }
505
506 #[test]
507 fn a_card_outranks_warnings_block() {
508 let mut o = Options::default();
509 o.apply_flag("-warnings-block").unwrap();
510 assert_eq!(o.object_code(), Compile::Until(Stop::W));
511 o.apply("NOCOMPILE(S)").unwrap();
512 assert_eq!(o.object_code(), Compile::Until(Stop::S));
513 o.apply("NOCOMPILE(E)").unwrap();
514 assert_eq!(o.object_code(), Compile::Until(Stop::E), "the last card wins");
515 }
516
517 #[test]
518 fn fastsrt_adv_print_excludes_unless_included() {
519 let mut o = Options::default();
520 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Exclude);
521 o.apply_flag("--fastsrt-adv-print=include").unwrap();
522 assert_eq!(o.fastsrt_adv_print, FastsrtAdvPrint::Include);
523 o.apply_flag("--fastsrt-adv-print=exclude").unwrap();
524 assert_eq!(o.fastsrt_adv_print.flag(), "--fastsrt-adv-print=exclude");
525 assert!(o.apply_flag("--fastsrt-adv-print=maybe").is_err());
526 assert!(o.apply_flag("--fastsrt-adv-print").is_err());
527 }
528}