1use crate::ast::*;
2use crate::lexer::{Tok, Token};
3use crate::messages::{IWS0097, IWS0098, IWS0100, IWS0104, IWX0013, IWX0014, IWX0017};
4use crate::{Error, Pos};
5
6mod communication;
7mod declaratives;
8mod oo;
9mod report;
10mod sort;
11
12fn scope(programs: &mut [Program]) {
16 let mut linked: Vec<String> = Vec::new();
17 for p in programs.iter().filter(|p| p.containers.is_empty()) {
18 linked.push(p.id.clone());
19 let entries = p.paragraphs.iter().flat_map(|q| &q.statements).filter_map(|s| match s {
20 crate::ast::Stmt::Entry { name, .. } => Some(name.clone()),
21 _ => None,
22 });
23 linked.extend(entries);
24 }
25 for p in programs.iter_mut() {
26 p.linked.clone_from(&linked);
27 }
28 let mut start = 0;
29 while start < programs.len() {
30 let end = programs[start + 1..].iter().position(|p| p.containers.is_empty()).map_or(programs.len(), |k| start + 1 + k);
31 let group = &programs[start..end];
32 let contained: Vec<String> = group[1..].iter().map(|p| p.id.clone()).collect();
33 let common = |id: &str| group.iter().find(|p| p.id == id).is_some_and(|p| p.common);
34 let children = |id: &str| group.iter().find(|p| p.id == id).map(|p| p.nested.clone()).unwrap_or_default();
35 let scopes: Vec<(Vec<String>, Vec<String>)> = group
36 .iter()
37 .map(|p| {
38 let enclosing: Vec<&str> = p.containers.iter().map(|c| c.id.as_str()).collect();
39 let mut callable = p.nested.clone();
40 for container in &enclosing {
41 for y in children(container) {
42 if common(&y) && y != p.id && !enclosing.contains(&y.as_str()) && !callable.contains(&y) {
43 callable.push(y);
44 }
45 }
46 }
47 let mut hidden: Vec<String> = contained.iter().filter(|c| !callable.contains(c)).cloned().collect();
48 let containers: Vec<String> = enclosing.iter().filter(|c| !hidden.iter().any(|h| h == *c)).map(|c| c.to_string()).collect();
49 hidden.extend(containers);
50 (callable, hidden)
51 })
52 .collect();
53 let mut seen = Vec::new();
54 let duplicates: Vec<(String, Pos)> = group.iter().filter_map(|p| if seen.contains(&p.id) { Some((p.id.clone(), p.pos)) } else { seen.push(p.id.clone()); None }).collect();
55 for (p, (callable, hidden)) in programs[start..end].iter_mut().zip(scopes) {
56 (p.callable, p.hidden) = (callable, hidden);
57 }
58 programs[start].duplicates = duplicates;
59 start = end;
60 }
61}
62
63pub fn parse(tokens: &[Token], options: Vec<String>, compliance: numeric::Compliance) -> Result<Vec<Program>, Error> {
67 let mut parser = Parser::new(tokens);
68 parser.extended = compliance == numeric::Compliance::Extended;
69 let mut programs = Vec::new();
70 parser.program(&options, &mut programs)?;
71 while parser.peek().is_some() {
72 if !parser.at_division(&["IDENTIFICATION", "ID"]) {
73 return Err(parser.error("another program, or the end of the source"));
74 }
75 parser.program(&options, &mut programs)?;
76 }
77 scope(&mut programs);
78 if let Some(first) = programs.iter().position(|p| p.function.is_none()) {
79 programs[..=first].rotate_right(1);
80 }
81 Ok(programs)
82}
83
84const VERBS: &[&str] = &[
86 "MOVE", "COMPUTE", "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "IF", "PERFORM", "DISPLAY", "INITIALIZE", "GO", "GOBACK", "STOP",
87 "CONTINUE", "EXIT", "EVALUATE", "SET", "CALL", "ACCEPT", "STRING", "UNSTRING", "INSPECT", "READ", "WRITE", "OPEN", "CLOSE",
88 "REWRITE", "DELETE", "START", "SEARCH", "SORT", "MERGE", "RETURN", "RELEASE", "CANCEL", "EXEC", "NEXT", "INVOKE",
89 "INITIATE", "GENERATE", "TERMINATE", "SUPPRESS", "ALTER", "ENTRY", "JSON", "XML", "ALLOCATE", "FREE",
90];
91
92fn relation(subject: Expr, op: RelOp, negated: bool, object: Expr) -> Cond {
95 let c = Cond::Rel(subject, op, object);
96 if negated { Cond::Not(Box::new(c)) } else { c }
97}
98
99fn digits(t: &Tok) -> bool {
101 matches!(t, Tok::Number(n) if n.bytes().all(|b| b.is_ascii_digit()))
102}
103
104const PHRASE_WORDS: &[&str] = &[
105 "ELSE", "END-IF", "END-PERFORM", "END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE", "END-DISPLAY", "END-ACCEPT", "WHEN",
106 "TO", "FROM", "BY", "INTO", "GIVING", "REMAINDER", "ROUNDED", "ON", "NOT", "SIZE", "UNTIL", "VARYING", "TIMES", "THRU", "THROUGH",
107 "AND", "OR", "THEN", "UPON", "WITH", "IS", "END-EVALUATE", "ALSO", "OTHER", "OF", "IN", "AT", "END", "END-READ", "END-WRITE",
108 "BEFORE", "AFTER", "ADVANCING", "INPUT", "OUTPUT", "EXTEND", "I-O", "REVERSED", "USING", "RETURNING", "EXCEPTION", "OVERFLOW",
109 "END-CALL", "OMITTED", "CONTENT", "REFERENCE", "VALUE", "UP", "DOWN", "DELIMITED", "DELIMITER", "COUNT", "POINTER", "TALLYING",
110 "REPLACING", "CONVERTING", "INITIAL", "FOR", "CHARACTERS", "LEADING", "FIRST", "ALL", "END-STRING", "END-UNSTRING", "END-SEARCH",
111 "NEXT", "INVALID", "KEY", "END-REWRITE", "END-DELETE", "END-START", "END-INVOKE", "END-RETURN", "END-OF-PAGE", "EOP", "END-JSON", "END-XML",
112];
113
114const SCOPE_TERMINATORS: &[&str] = &[
117 "END-ACCEPT", "END-ADD", "END-CALL", "END-COMPUTE", "END-DELETE", "END-DISPLAY", "END-DIVIDE", "END-EVALUATE", "END-IF", "END-INVOKE",
118 "END-JSON", "END-MULTIPLY", "END-PERFORM", "END-READ", "END-RETURN", "END-REWRITE", "END-SEARCH", "END-START", "END-STRING",
119 "END-SUBTRACT", "END-UNSTRING", "END-WRITE", "END-XML",
120];
121
122const JSON_PHRASES: &[&str] = &["COUNT", "INDICATING", "ENCODING", "NAME", "SUPPRESS", "CONVERTING", "IGNORING", "WITH", "DETAIL", "ON", "NOT", "EXCEPTION", "END-JSON", "ALSO"];
125
126const XML_PHRASES: &[&str] =
128 &["COUNT", "WITH", "ENCODING", "XML-DECLARATION", "ATTRIBUTES", "NAMESPACE", "NAMESPACE-PREFIX", "NAME", "TYPE", "SUPPRESS", "EVERY", "ON", "NOT", "EXCEPTION", "END-XML"];
129
130pub(crate) const UNTAKEN_INTRINSIC_NAMES: &[&str] = &["LENGTH", "RANDOM", "SIGN", "SUM", "WHEN-COMPILED"];
135pub const DEVICE_ENVIRONMENT_NAMES: &[&str] = &["SYSIN", "SYSIPT", "SYSOUT", "SYSLIST", "SYSLST", "SYSPUNCH", "SYSPCH", "CONSOLE"];
136
137const ACCEPT_DEVICES: &[&str] = &["SYSIN", "SYSIPT", "CONSOLE"];
139
140const GNUCOBOL_ACCEPT_SOURCES: &[&str] = &["ESCAPE", "EXCEPTION", "LINES", "COLUMNS", "COLS", "CRT", "USER"];
142
143fn advancing_environment_name(word: &str) -> bool {
146 let numbered = |prefix: char, last: u8| word.len() == 3 && word.starts_with(prefix) && word[1..].parse::<u8>().is_ok_and(|n| (1..=last).contains(&n));
147 matches!(word, "CSP" | "AFP-5A") || numbered('C', 12) || numbered('S', 5)
148}
149
150fn upsi_switch(word: &str) -> Option<u8> {
152 match word.strip_prefix("UPSI-")?.as_bytes() {
153 [d @ b'0'..=b'7'] => Some(d - b'0'),
154 _ => None,
155 }
156}
157
158fn fixed_point(mantissa: &str, exponent: i32) -> Option<String> {
160 let (sign, body) = match mantissa.strip_prefix('-') {
161 Some(body) => ("-", body),
162 None => ("", mantissa.trim_start_matches('+')),
163 };
164 let (int, frac) = body.split_once('.')?;
165 let digits = format!("{int}{frac}");
166 let point = int.len() as i64 + i64::from(exponent);
167 let (int, frac) = match usize::try_from(point) {
168 Err(_) => (String::new(), "0".repeat(point.unsigned_abs() as usize) + &digits),
169 Ok(p) if p >= digits.len() => (digits.clone() + &"0".repeat(p - digits.len()), String::new()),
170 Ok(p) => (digits[..p].to_owned(), digits[p..].to_owned()),
171 };
172 let (int, frac) = (int.trim_start_matches('0'), frac.trim_end_matches('0'));
173 let int = if int.is_empty() { "0" } else { int };
174 (int.len() + frac.len() <= 31).then(|| if frac.is_empty() { format!("{sign}{int}") } else { format!("{sign}{int}.{frac}") })
175}
176
177fn figurative(word: &str) -> Option<Figurative> {
178 Some(match word {
179 "ZERO" | "ZEROS" | "ZEROES" => Figurative::Zero,
180 "SPACE" | "SPACES" => Figurative::Space,
181 "HIGH-VALUE" | "HIGH-VALUES" => Figurative::HighValue,
182 "LOW-VALUE" | "LOW-VALUES" => Figurative::LowValue,
183 "QUOTE" | "QUOTES" => Figurative::Quote,
184 "NULL" | "NULLS" => Figurative::Null,
185 _ => return None,
186 })
187}
188
189fn currency_symbol(c: char) -> bool {
192 u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz +-,.*/;()\"='".contains(c)
193}
194
195fn hex_text(bytes: &[u8]) -> String {
196 format!("X'{}'", bytes.iter().map(|b| format!("{b:02X}")).collect::<String>())
197}
198
199pub fn decode_currency(environment: &mut Environment, decode: impl Fn(&[u8]) -> String) -> Result<(), crate::messages::Refused> {
202 for k in 0..environment.currency.len() {
203 let sign = &mut environment.currency[k];
204 let Some(bytes) = &sign.hex else { continue };
205 let (value, shown) = (decode(bytes), hex_text(bytes));
206 if sign.symbol == HEX_SYMBOL {
207 match value.chars().next() {
208 Some(symbol) if currency_symbol(symbol) => sign.symbol = symbol,
209 _ => return Err((crate::messages::IWS0095, format!("CURRENCY SIGN {shown} is {value:?} in the program's code page, which cannot be a PICTURE currency symbol"))),
210 }
211 } else if value.chars().any(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | '.' | ',')) {
212 return Err((crate::messages::IWS0096, format!("CURRENCY SIGN {shown} is {value:?} in the program's code page, which contains a digit, +, -, . or ,")));
213 }
214 sign.value = value;
215 sign.hex = None;
216 let symbol = sign.symbol;
217 if environment.currency.iter().filter(|c| c.symbol == symbol).count() > 1 {
218 return Err((crate::messages::IWS0036, format!("a second CURRENCY SIGN clause for the currency symbol {symbol:?}")));
219 }
220 }
221 Ok(())
222}
223
224fn share_configuration(outer: &Environment, inner: &mut Environment) {
228 inner.debugging_mode |= outer.debugging_mode;
229 inner.decimal_point_comma |= outer.decimal_point_comma;
230 if inner.currency.is_empty() {
231 inner.currency.clone_from(&outer.currency);
232 }
233 if inner.collating_sequence.is_none() {
234 inner.collating_sequence.clone_from(&outer.collating_sequence);
235 }
236 for (name, alphabet) in &outer.alphabets {
237 if !inner.alphabets.iter().any(|(n, _)| n == name) {
238 inner.alphabets.push((name.clone(), alphabet.clone()));
239 }
240 }
241}
242
243fn global_records(entries: &[DataEntry]) -> Vec<DataEntry> {
245 let mut out = Vec::new();
246 let mut taking = false;
247 for e in entries {
248 if matches!(e.level, 1 | 77) {
249 taking = e.global;
250 }
251 if taking {
252 out.push(e.clone());
253 }
254 }
255 out
256}
257
258fn function_name(name: &str) -> Result<(), &'static str> {
261 if name.len() > 30 {
262 return Err("a function name has at most 30 characters");
263 }
264 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') || !name.chars().any(|c| c.is_ascii_alphabetic()) {
265 return Err("a function name is letters, digits, hyphens and underscores, with a letter among them");
266 }
267 if name.starts_with('-') || name.ends_with('-') {
268 return Err("a function name neither starts nor ends with a hyphen");
269 }
270 if figurative(name).is_some() {
271 return Err("a figurative constant cannot name a function");
272 }
273 Ok(())
274}
275
276fn environment_display(item: Operand, written: &str, device: &str, pos: Pos) -> Stmt {
279 Stmt::Display { items: vec![item], upon: Some(Upon { name: written.to_owned(), device: device.to_owned() }), no_advancing: false, screen: None, pos }
280}
281
282const CRT_DEVICES: &[&str] = &["CRT", "CRT-UNDER"];
285
286const SCREEN_CLAUSES: &[&str] = &[
288 "LINE", "COL", "COLUMN", "VALUE", "PIC", "PICTURE", "FROM", "TO", "USING", "BLANK", "ERASE", "SECURE", "NO-ECHO", "OCCURS", "JUSTIFIED", "JUST", "SIGN", "USAGE",
289 "PROMPT", "COLOR", "COLOUR", "HIGHLIGHT", "LOWLIGHT", "REVERSE-VIDEO", "BLINK", "UNDERLINE", "BELL", "BEEP", "AUTO", "AUTO-SKIP", "AUTOTERMINATE", "FULL",
290 "LENGTH-CHECK", "REQUIRED", "EMPTY-CHECK", "UPPER", "LOWER", "OVERLINE", "LEFTLINE", "GRID", "FOREGROUND-COLOR", "FOREGROUND-COLOUR", "BACKGROUND-COLOR",
291 "BACKGROUND-COLOUR", "SIZE", "CONTROL",
292];
293
294const SCREEN_ATTRIBUTES: &[&str] = &[
296 "HIGHLIGHT", "LOWLIGHT", "REVERSE-VIDEO", "BLINK", "UNDERLINE", "BELL", "BEEP", "AUTO", "AUTO-SKIP", "AUTOTERMINATE", "FULL", "LENGTH-CHECK", "REQUIRED",
297 "EMPTY-CHECK", "UPPER", "LOWER", "NO-ECHO", "OVERLINE", "LEFTLINE", "GRID", "PROMPT", "FOREGROUND-COLOR", "FOREGROUND-COLOUR", "BACKGROUND-COLOR",
298 "BACKGROUND-COLOUR", "TIMEOUT", "TIME-OUT", "SIZE", "CONTROL", "SCROLL",
299];
300
301fn gnucobol_float(word: &str, pos: Pos) -> Option<Error> {
303 if word == "PROGRAM-POINTER" {
304 return Some(crate::messages::IWC0314.at(pos, "PROGRAM-POINTER: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's; --compliance extended reads it as PROCEDURE-POINTER"));
305 }
306 let ibm = match word {
307 "FLOAT-SHORT" => "COMP-1",
308 "FLOAT-LONG" => "COMP-2",
309 _ => return None,
310 };
311 Some(crate::messages::IWS0101.at(pos, format!("{word}: GnuCOBOL's and Micro Focus's floating point, not Enterprise COBOL's; --compliance extended reads it as {ibm}")))
312}
313
314pub const CRT_STATUS: &str = "COB-CRT-STATUS";
316
317fn crt_status_entry(pos: Pos) -> DataEntry {
319 DataEntry {
320 level: 77,
321 name: Some(CRT_STATUS.into()),
322 spelled: None,
323 picture: Some("9(4)".into()),
324 usage: None,
325 value: Some(Literal::Number("0".into())),
326 redefines: None,
327 occurs: None,
328 occurs_min: None,
329 depending_on: None,
330 sign: None,
331 justified: false,
332 sync: false,
333 blank_when_zero: false,
334 indexed_by: Vec::new(),
335 keys: Vec::new(),
336 condition_values: Vec::new(),
337 false_value: None,
338 renames: None,
339 object_class: None,
340 external: false,
341 global: false,
342 any_length: false,
343 based: false,
344 pos,
345 }
346}
347
348fn usage_word(word: &str) -> Option<Usage> {
349 Some(match word {
350 "DISPLAY" => Usage::Display,
351 "BINARY" | "COMP" | "COMPUTATIONAL" | "COMP-4" | "COMPUTATIONAL-4" => Usage::Binary,
352 "COMP-5" | "COMPUTATIONAL-5" => Usage::NativeBinary,
353 "COMP-X" | "COMPUTATIONAL-X" => Usage::CompX,
354 "BINARY-CHAR" => Usage::BinaryChar { signed: true },
355 "PACKED-DECIMAL" | "COMP-3" | "COMPUTATIONAL-3" => Usage::Packed,
356 "COMP-1" | "COMPUTATIONAL-1" => Usage::Float1,
357 "COMP-2" | "COMPUTATIONAL-2" => Usage::Float2,
358 "NATIONAL" => Usage::National,
359 "DISPLAY-1" => Usage::Dbcs,
360 "POINTER" => Usage::Pointer,
361 "INDEX" => Usage::Index,
362 "FUNCTION-POINTER" | "PROCEDURE-POINTER" => Usage::ProgramPointer,
363 _ => return None,
364 })
365}
366
367struct Parser<'a> {
368 tokens: &'a [Token],
369 at: usize,
370 before: Vec<Stmt>,
373 exec_declarations: Vec<ExecBlock>,
375 cics: bool,
377 dli: bool,
379 intrinsics: Vec<String>,
382 repository_functions: Vec<String>,
385 mnemonics: Vec<(String, String)>,
389 switches: Vec<Switch>,
392 classes: Vec<ClassClause>,
394 sql: SqlState,
395 debugging: bool,
397 messages: Vec<Error>,
399 open: Vec<String>,
403 registers: Vec<String>,
405 reported: Vec<bool>,
408 functions: Vec<Prototype>,
410 defined: Vec<String>,
411 in_prototype: bool,
413 extended: bool,
415 cds: Vec<String>,
417 unstored: Vec<String>,
420}
421
422#[derive(Default)]
425struct SqlState {
426 whenever: crate::sql::Whenever,
427 blocks: u32,
428 cursors: crate::sql::Cursors,
429}
430
431type R<T> = Result<T, Error>;
432
433impl<'a> Parser<'a> {
434 fn new(tokens: &'a [Token]) -> Self {
435 Self {
436 tokens,
437 at: 0,
438 exec_declarations: Vec::new(),
439 before: Vec::new(),
440 cics: false,
441 dli: false,
442 intrinsics: Vec::new(),
443 repository_functions: Vec::new(),
444 sql: SqlState::default(),
445 mnemonics: Vec::new(),
446 switches: Vec::new(),
447 classes: Vec::new(),
448 debugging: false,
449 messages: Vec::new(),
450 open: Vec::new(),
451 registers: Vec::new(),
452 reported: vec![false; tokens.len()],
453 functions: Vec::new(),
454 defined: Vec::new(),
455 in_prototype: false,
456 extended: false,
457 cds: Vec::new(),
458 unstored: Vec::new(),
459 }
460 }
461}
462
463impl Parser<'_> {
464 fn peek(&self) -> Option<&Tok> {
465 self.tokens.get(self.at).map(|t| &t.tok)
466 }
467
468 fn peek_at(&self, ahead: usize) -> Option<&Tok> {
469 self.tokens.get(self.at + ahead).map(|t| &t.tok)
470 }
471
472 fn qualifying_paren_at(&self, at: usize) -> bool {
475 self.tokens.get(at).is_some_and(|t| t.tok == Tok::LParen && !t.after_comma)
476 }
477
478 fn pos(&self) -> Pos {
479 self.tokens.get(self.at).or(self.tokens.last()).map(|t| t.pos).unwrap_or_default()
480 }
481
482 fn word(&self) -> Option<&str> {
483 match self.peek() {
484 Some(Tok::Word(w)) => Some(w),
485 _ => None,
486 }
487 }
488
489 fn word_at(&self, ahead: usize) -> Option<&str> {
490 match self.peek_at(ahead) {
491 Some(Tok::Word(w)) => Some(w),
492 _ => None,
493 }
494 }
495
496 fn is_word(&self, w: &str) -> bool {
497 self.word() == Some(w)
498 }
499
500 fn accept_word(&mut self, w: &str) -> bool {
501 let yes = self.is_word(w);
502 if yes {
503 self.at += 1;
504 }
505 yes
506 }
507
508 fn accept_any(&mut self, words: &[&str]) -> Option<String> {
509 let w = self.word().filter(|w| words.contains(w))?.to_owned();
510 self.at += 1;
511 Some(w)
512 }
513
514 fn expect_word(&mut self, w: &str) -> R<()> {
515 if self.accept_word(w) { Ok(()) } else { Err(self.error(format!("expected {w}"))) }
516 }
517
518 fn accept(&mut self, tok: &Tok) -> bool {
519 let yes = self.peek() == Some(tok);
520 if yes {
521 self.at += 1;
522 }
523 yes
524 }
525
526 fn expect(&mut self, tok: &Tok, what: &str) -> R<()> {
527 if self.accept(tok) { Ok(()) } else { Err(self.error(format!("expected {what}"))) }
528 }
529
530 fn error(&self, message: impl Into<String>) -> Error {
531 let found = match self.peek() {
532 None => "end of source".to_owned(),
533 Some(Tok::Word(w)) => w.clone(),
534 Some(t) => format!("{t:?}"),
535 };
536 crate::messages::IWS0001.at(self.pos(), format!("{}, found {found}", message.into()))
537 }
538
539 fn name(&mut self, what: &str) -> R<String> {
540 match self.peek() {
541 Some(Tok::Word(w)) => {
542 let w = w.clone();
543 self.at += 1;
544 Ok(w)
545 }
546 _ => Err(self.error(format!("expected {what}"))),
547 }
548 }
549
550 fn at_division(&self, names: &[&str]) -> bool {
551 self.word().is_some_and(|w| names.contains(&w)) && self.word_at(1) == Some("DIVISION")
552 }
553
554 fn program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
555 let (start, first) = (self.at, out.len());
556 let outer = (std::mem::take(&mut self.exec_declarations), std::mem::take(&mut self.cics), std::mem::take(&mut self.dli), std::mem::take(&mut self.sql.blocks), self.mnemonics.clone(), self.debugging);
557 let outer_messages = std::mem::take(&mut self.messages);
558 let outer_registers = std::mem::take(&mut self.registers);
559 let (outer_intrinsics, outer_functions, outer_switches, outer_classes) = (self.intrinsics.clone(), self.repository_functions.clone(), self.switches.clone(), self.classes.clone());
560 let parsed = self.one_program(options, out);
561 (self.intrinsics, self.repository_functions, self.switches, self.classes) = (outer_intrinsics, outer_functions, outer_switches, outer_classes);
562 (self.exec_declarations, self.cics, self.dli, self.sql.blocks, self.mnemonics, self.debugging) = outer;
563 let own = std::mem::replace(&mut self.messages, outer_messages);
564 self.registers = outer_registers;
565 parsed?;
566 let mut messages = Vec::new();
567 for i in start..self.at {
568 if !std::mem::replace(&mut self.reported[i], true) {
569 messages.extend(self.tokens[i].messages.iter().cloned());
570 }
571 }
572 messages.extend(own);
573 if let Some(p) = out.get_mut(first) {
574 p.messages = messages;
575 }
576 Ok(())
577 }
578
579 fn one_program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
580 if !self.accept_word("IDENTIFICATION") {
581 self.expect_word("ID")?;
582 }
583 self.expect_word("DIVISION")?;
584 self.expect(&Tok::Period, "a period")?;
585 if self.is_word("CLASS-ID") {
586 return self.class_definition(options, out);
587 }
588 if self.is_word("FUNCTION-ID") {
589 return self.function_definition(options, out);
590 }
591 self.expect_word("PROGRAM-ID")?;
592 self.accept(&Tok::Period);
593 let named_at = self.pos();
594 let id = match self.peek() {
595 Some(Tok::Alnum(s)) => {
596 let s = s.clone();
597 self.at += 1;
598 s
599 }
600 _ => self.name("a program name")?,
601 };
602 let (mut initial, mut recursive, mut common) = (false, false, false);
603 while let Some(t) = self.peek() {
604 if *t == Tok::Period {
605 self.at += 1;
606 break;
607 }
608 initial |= self.is_word("INITIAL");
609 recursive |= self.is_word("RECURSIVE");
610 common |= self.is_word("COMMON");
611 self.at += 1;
612 }
613 let first = out.len();
614 self.program_body(id, initial, recursive, options, out, false)?;
615 (out[first].common, out[first].pos) = (common, named_at);
616 Ok(())
617 }
618
619 fn program_body(&mut self, id: String, initial: bool, recursive: bool, options: &[String], out: &mut Vec<Program>, method: bool) -> R<()> {
622 while self.peek().is_some() && !self.at_division(&["ENVIRONMENT", "DATA", "PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() {
623 self.at += 1;
624 }
625 let (mut files, mut repository, mut environment) = (Vec::new(), Vec::new(), Environment::default());
626 if self.at_division(&["ENVIRONMENT"]) {
627 (files, repository) = self.environment(&mut environment)?;
628 }
629 self.mnemonics.splice(0..0, environment.mnemonics.iter().cloned());
630 self.switches.splice(0..0, std::mem::take(&mut environment.switches));
631 environment.switches = self.switches.clone();
632 self.classes.splice(0..0, std::mem::take(&mut environment.classes));
633 environment.classes = self.classes.clone();
634 self.debugging |= environment.debugging_mode;
635 let (mut working_storage, mut local_storage, mut linkage, mut screens) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
636 let mut report_writer = crate::report::ReportWriter::default();
637 let mut declaratives = Declaratives::default();
638 let mut communication = Vec::new();
639 if self.at_division(&["DATA"]) {
640 self.at += 2;
641 self.expect(&Tok::Period, "a period")?;
642 self.cds.clear();
643 while !self.at_division(&["PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() && self.peek().is_some() {
644 if self.data_exec()? {
645 continue;
646 }
647 let header = self.pos();
648 if matches!(self.word(), Some("FD" | "SD")) {
649 if !self.extended {
650 return Err(crate::messages::IWC0310.at(header, "a file description with no FILE SECTION header: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it"));
651 }
652 self.messages.push(crate::messages::IWX0037.at(header, "a file description with no FILE SECTION header (Micro Focus and GnuCOBOL; Enterprise COBOL writes FILE SECTION first): it is read as though FILE SECTION came first"));
653 self.file_section(&mut files)?;
654 continue;
655 }
656 let section = self.name("a DATA DIVISION section")?;
657 self.expect_word("SECTION")?;
658 self.expect(&Tok::Period, "a period")?;
659 match section.as_str() {
660 "WORKING-STORAGE" => working_storage = self.data_entries()?,
661 "LINKAGE" => linkage = self.data_entries()?,
662 "LOCAL-STORAGE" => local_storage = self.data_entries()?,
663 "FILE" => self.file_section(&mut files)?,
664 "REPORT" => report_writer.reports.extend(self.report_section()?),
665 "SCREEN" => screens = self.screen_section()?,
666 "COMMUNICATION" => communication = self.communication_section(header)?,
667 other => return Err(crate::messages::IWR0006.at(self.pos(), format!("the {other} SECTION is not supported yet"))),
668 }
669 }
670 }
671 working_storage.extend(communication);
672 let records = linkage.iter().chain(working_storage.iter().chain(&local_storage).filter(|e| e.based));
673 self.unstored = records.filter(|e| matches!(e.level, 1 | 77)).filter_map(|e| e.name.clone()).collect();
674 let (mut using, mut returning) = (Vec::new(), None);
675 let procedure_from = self.at;
676 let paragraphs = if self.at_division(&["PROCEDURE"]) {
677 self.at += 2;
678 if self.accept_word("USING") {
679 using = self.parameters()?;
680 }
681 let chaining = self.chaining()?;
682 if self.accept_word("RETURNING") {
683 returning = Some(self.name("a RETURNING item")?);
684 }
685 self.expect(&Tok::Period, "a period after the PROCEDURE DIVISION header")?;
686 let mut paragraphs = self.procedure_paragraphs(&mut report_writer, &mut declaratives)?;
687 if let Some(first) = paragraphs.get_mut(report_writer.procedure_start) {
688 first.statements.splice(0..0, chaining);
689 }
690 paragraphs
691 } else {
692 Vec::new()
693 };
694 declaratives::debugging_sections_allowed(&declaratives, recursive, method)?;
695 if let Some(f) = files.iter().find(|f| f.assign.is_empty()) {
696 return Err(crate::messages::IWS0028.at(f.pos, format!("{} has no SELECT ... ASSIGN", f.name)));
697 }
698 if self.cics {
699 self.translator_additions(&mut linkage, &mut using)?;
700 }
701 let declares = |entries: &[DataEntry], name: &str| entries.iter().any(|e| e.name.as_deref() == Some(name));
702 if self.dli && !declares(&working_storage, "DIBSTAT") && !declares(&linkage, "DIBSTAT") {
703 working_storage.splice(0..0, system_entries("DLZDIB")?);
704 }
705 let names_crt_status = self.tokens[procedure_from..self.at].iter().any(|t| matches!(&t.tok, Tok::Word(w) if w == CRT_STATUS));
706 if self.extended && names_crt_status && !declares(&working_storage, CRT_STATUS) && !declares(&local_storage, CRT_STATUS) && !declares(&linkage, CRT_STATUS) {
707 let pos = self.tokens[procedure_from..self.at].iter().find(|t| matches!(&t.tok, Tok::Word(w) if w == CRT_STATUS)).map_or_else(Pos::default, |t| t.pos);
708 self.messages.push(crate::messages::IWX0057.at(pos, "COB-CRT-STATUS (GnuCOBOL's special register; Enterprise COBOL has no screen ACCEPT): it holds the key that ended the last screen ACCEPT, as GnuCOBOL's screenio.cpy numbers the keys"));
709 working_storage.push(crt_status_entry(pos));
710 }
711 let exec_declarations = std::mem::take(&mut self.exec_declarations);
712 let (mut nested, mut contained) = (Vec::new(), Vec::new());
713 while self.at_division(&["IDENTIFICATION", "ID"]) {
714 if self.word_at(3) == Some("FUNCTION-ID") {
715 return Err(crate::messages::IWS0029.at(self.tokens[self.at + 3].pos, "a user-defined function or prototype cannot be nested within a program, function, method or class"));
716 }
717 let first = nested.len();
718 self.program(options, &mut nested)?;
719 contained.extend(nested.get(first).map(|p: &Program| p.id.clone()));
720 }
721 let entry = nested.iter().flat_map(|p: &Program| &p.paragraphs).flat_map(|p| &p.statements).find_map(|s| match s {
722 Stmt::Entry { pos, .. } => Some(*pos),
723 _ => None,
724 });
725 if let Some(pos) = entry {
726 return Err(crate::messages::IWS0030.at(pos, "ENTRY cannot be used in a nested program"));
727 }
728 oo::share_repository(&repository, &mut nested)?;
729 let container = Container {
730 id: id.clone(),
731 working_storage: global_records(&working_storage),
732 local_storage: global_records(&local_storage),
733 linkage: global_records(&linkage),
734 files: files.iter().filter(|f| f.global).cloned().collect(),
735 };
736 for inner in &mut nested {
737 share_configuration(&environment, &mut inner.environment);
738 inner.containers.push(container.clone());
739 }
740 declaratives::contained_programs(&report_writer, &nested)?;
741 if !method && self.at_end_program() && self.word_at(1) == Some("PROGRAM") {
742 self.at += 2;
743 if self.word().is_some() || matches!(self.peek(), Some(Tok::Alnum(_))) {
744 self.at += 1;
745 }
746 self.accept(&Tok::Period);
747 }
748 out.push(Program {
749 id,
750 options: options.to_vec(),
751 initial,
752 recursive,
753 working_storage,
754 local_storage,
755 linkage,
756 using,
757 returning,
758 paragraphs,
759 files,
760 exec_declarations,
761 report_writer,
762 declaratives,
763 oo: oo::program_oo(repository),
764 environment,
765 nested: contained,
766 registers: std::mem::take(&mut self.registers),
767 prototypes: self.functions.clone(),
768 repository_functions: self.repository_functions.clone(),
769 screens,
770 ..Program::default()
771 });
772 out.extend(nested);
773 Ok(())
774 }
775
776 fn function_definition(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
779 let pos = self.pos();
780 self.at += 1;
781 self.accept(&Tok::Period);
782 let name = self.name("a function name")?;
783 function_name(&name).map_err(|why| crate::messages::IWS0031.at(self.tokens[self.at - 1].pos, format!("FUNCTION-ID {name}: {why}")))?;
784 if UNTAKEN_INTRINSIC_NAMES.contains(&name.as_str()) {
785 return Err(crate::messages::IWS0032.at(pos, format!("FUNCTION-ID {name}: a user-defined function cannot be named {name}")));
786 }
787 let (mut external, mut prototype) = (name.clone(), false);
788 while !self.accept(&Tok::Period) {
789 if self.accept_word("AS") {
790 match self.peek() {
791 Some(Tok::Alnum(s)) if !s.trim().is_empty() => external = s.clone(),
792 _ => return Err(self.error("the function's external name, an alphanumeric literal, after AS")),
793 }
794 self.at += 1;
795 } else if self.accept_word("IS") || self.is_word("PROTOTYPE") {
796 self.expect_word("PROTOTYPE")?;
797 prototype = true;
798 } else if let Some(phrase) = self.accept_any(&["ENTRY-NAME", "ENTRY-INTERFACE"]) {
799 self.accept_word("IS");
800 let allowed: &[&str] = if phrase == "ENTRY-NAME" { &["COMPAT", "LONGUPPER", "LONGMIXED"] } else { &["STATIC", "DYNAMIC", "DLL"] };
801 if self.accept_any(allowed).is_none() {
802 return Err(self.error(format!("{} after {phrase}", allowed.join(", "))));
803 }
804 } else {
805 return Err(self.error("AS, IS PROTOTYPE, ENTRY-NAME, ENTRY-INTERFACE or the period ending the FUNCTION-ID paragraph"));
806 }
807 }
808 let external = numeric::Pgmname::of(options).external(&external);
809 if !prototype && self.defined.contains(&name) {
810 return Err(crate::messages::IWS0033.at(pos, format!("a second definition of user-defined function {name}")));
811 }
812 let first = out.len();
813 self.in_prototype = prototype;
814 let body = self.program_body(name.clone(), false, true, options, out, false);
815 self.in_prototype = false;
816 body?;
817 let program = &mut out[first];
818 program.function = Some(Function { external, prototype, pos });
819 if let Some(inner) = program.nested.first() {
820 return Err(crate::messages::IWS0034.at(pos, format!("FUNCTION-ID {name}: a user-defined function contains no programs, but {inner} is inside it")));
821 }
822 if !(self.at_end_program() && self.word_at(1) == Some("FUNCTION")) {
823 return Err(self.error(format!("END FUNCTION {name}, which ends a user-defined function")));
824 }
825 self.at += 2;
826 let end = self.name("the function name after END FUNCTION")?;
827 if end != name {
828 return Err(crate::messages::IWS0035.at(self.tokens[self.at - 1].pos, format!("END FUNCTION {end} ends function {name}")));
829 }
830 self.accept(&Tok::Period);
831 let own = Prototype {
832 name,
833 external: program.load_name().to_owned(),
834 using: program.using.clone(),
835 returning: program.returning.clone(),
836 linkage: program.linkage.clone(),
837 environment: program.environment.clone(),
838 pos,
839 };
840 program.prototypes.push(own.clone());
841 self.functions.push(own);
842 if !prototype {
843 self.defined.push(program.id.clone());
844 }
845 Ok(())
846 }
847
848 fn parameters(&mut self) -> R<Vec<Param>> {
850 let (mut using, mut by_value) = (Vec::new(), false);
851 loop {
852 if self.accept_word("BY") {
853 by_value = self.accept_any(&["REFERENCE", "VALUE"]).as_deref() == Some("VALUE");
854 continue;
855 }
856 if !self.starts_ref() {
857 return Ok(using);
858 }
859 using.push(Param { by_value, name: self.name("a LINKAGE item")? });
860 }
861 }
862
863 fn at_end_program(&self) -> bool {
864 self.is_word("END") && matches!(self.word_at(1), Some("PROGRAM" | "METHOD" | "FUNCTION"))
865 }
866
867 fn environment(&mut self, clauses: &mut Environment) -> R<(Vec<FileDecl>, Vec<ClassEntry>)> {
870 let (mut files, mut repository) = (Vec::new(), Vec::new());
871 let mut special_names = false;
872 while self.peek().is_some() && !self.at_division(&["DATA", "PROCEDURE"]) {
873 if let Some(paragraph) = self.word().filter(|w| matches!(*w, "SPECIAL-NAMES" | "REPOSITORY" | "SOURCE-COMPUTER" | "OBJECT-COMPUTER" | "INPUT-OUTPUT" | "FILE-CONTROL" | "I-O-CONTROL")) {
874 special_names = paragraph == "SPECIAL-NAMES";
875 }
876 if self.accept_word("DECIMAL-POINT") {
877 self.accept_word("IS");
878 self.expect_word("COMMA")?;
879 clauses.decimal_point_comma = true;
880 continue;
881 }
882 if self.is_word("CURRENCY") {
883 let pos = self.pos();
884 self.at += 1;
885 let sign = self.currency_sign()?;
886 if sign.symbol != HEX_SYMBOL && clauses.currency.iter().any(|c| c.symbol == sign.symbol) {
887 return Err(crate::messages::IWS0036.at(pos, format!("a second CURRENCY SIGN clause for the currency symbol {:?}", sign.symbol)));
888 }
889 clauses.currency.push(sign);
890 continue;
891 }
892 if self.environment_clause(clauses)? {
893 continue;
894 }
895 if self.is_word("DEBUGGING") && self.word_at(1) == Some("MODE") {
896 self.at += 2;
897 clauses.debugging_mode = true;
898 continue;
899 }
900 if special_names && self.is_word("CLASS") && self.word_at(1).is_some() {
901 let pos = self.pos();
902 self.at += 1;
903 clauses.classes.push(self.class_clause(pos)?);
904 continue;
905 }
906 if let Some(number) = self.word().and_then(upsi_switch) {
907 let pos = self.pos();
908 self.at += 1;
909 clauses.switches.push(self.switch(number, pos)?);
910 continue;
911 }
912 if let Some(environment) = self.word().filter(|w| advancing_environment_name(w) || DEVICE_ENVIRONMENT_NAMES.contains(w)).map(str::to_owned) {
913 let at_name = if self.word_at(1) == Some("IS") { 2 } else { 1 };
914 if let Some(name) = self.word_at(at_name).map(str::to_owned) {
915 self.at += at_name + 1;
916 clauses.mnemonics.push((name, environment));
917 continue;
918 }
919 }
920 if self.accept_word("SELECT") {
921 files.push(self.select()?);
922 continue;
923 }
924 if self.is_word("REPOSITORY") && self.in_prototype {
925 return Err(self.error("no REPOSITORY paragraph: a function prototype cannot have one"));
926 }
927 if self.accept_word("REPOSITORY") {
928 repository = self.repository()?;
929 continue;
930 }
931 self.at += 1;
932 }
933 Ok((files, repository))
934 }
935
936 fn class_clause(&mut self, pos: Pos) -> R<ClassClause> {
943 let name = self.name("a class-name")?;
944 self.accept_word("IS");
945 let mut members = Vec::new();
946 while matches!(self.peek(), Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::Number(_) | Tok::National(_) | Tok::Dbcs(_))) {
947 let first = self.literal()?;
948 let last = if self.accept_any(&["THROUGH", "THRU"]).is_some() { Some(self.literal()?) } else { None };
949 members.push((first, last));
950 }
951 if members.is_empty() {
952 return Err(self.error(format!("a literal after CLASS {name}")));
953 }
954 Ok(ClassClause { name, members, pos })
955 }
956
957 fn switch(&mut self, number: u8, pos: Pos) -> R<Switch> {
958 let status = |w: &str| matches!(w, "ON" | "OFF");
959 let mnemonic = if self.accept_word("IS") || self.word().is_some_and(|w| !status(w) && !rt::reserved_words::is_reserved(w)) {
960 Some(self.name("a mnemonic-name")?)
961 } else {
962 None
963 };
964 let (mut on, mut off) = (None, None);
965 while let Some(which) = self.accept_any(&["ON", "OFF"]) {
966 self.accept_word("STATUS");
967 self.accept_word("IS");
968 let name = self.name("a condition-name")?;
969 let slot = if which == "ON" { &mut on } else { &mut off };
970 if slot.replace(name).is_some() {
971 return Err(crate::messages::IWS0037.at(pos, format!("UPSI-{number}: a second {which} STATUS phrase")));
972 }
973 }
974 if mnemonic.is_none() && on.is_none() && off.is_none() {
975 return Err(crate::messages::IWS0038.at(pos, format!("UPSI-{number}: a mnemonic-name or an ON or OFF STATUS phrase must follow it")));
976 }
977 Ok(Switch { number, mnemonic, on, off, pos })
978 }
979
980 fn currency_sign(&mut self) -> R<CurrencySign> {
983 self.accept_word("SIGN");
984 self.accept_word("IS");
985 let pos = self.pos();
986 let (value, hex) = match self.literal()? {
987 Literal::Alnum(v) if !v.is_empty() => (v, None),
988 Literal::Hex(b) if !b.is_empty() => (String::new(), Some(b)),
989 _ => return Err(crate::messages::IWS0039.at(pos, "CURRENCY SIGN needs a nonempty alphanumeric literal")),
990 };
991 let with = self.accept_word("WITH");
992 if !(self.is_word("PICTURE") && self.word_at(1) == Some("SYMBOL")) {
993 if with {
994 return Err(self.error("expected PICTURE SYMBOL after WITH"));
995 }
996 if let Some(bytes) = hex {
997 return match bytes.len() {
998 1 => Ok(CurrencySign { value, symbol: HEX_SYMBOL, hex: Some(bytes) }),
999 _ => Err(crate::messages::IWS0040.at(pos, format!("CURRENCY SIGN {} is not one character that can be a PICTURE currency symbol", hex_text(&bytes)))),
1000 };
1001 }
1002 let mut chars = value.chars();
1003 return match (chars.next(), chars.next()) {
1004 (Some(symbol), None) if currency_symbol(symbol) => Ok(CurrencySign { value, symbol, hex }),
1005 _ => Err(crate::messages::IWS0041.at(pos, format!("CURRENCY SIGN {value:?} is not one character that can be a PICTURE currency symbol"))),
1006 };
1007 }
1008 self.at += 2;
1009 if value.chars().any(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | '.' | ',')) {
1010 return Err(crate::messages::IWS0042.at(pos, format!("CURRENCY SIGN {value:?} contains a digit, +, -, . or ,")));
1011 }
1012 let pos = self.pos();
1013 let symbol = match self.literal()? {
1014 Literal::Alnum(s) => s,
1015 _ => String::new(),
1016 };
1017 let mut chars = symbol.chars();
1018 match (chars.next(), chars.next()) {
1019 (Some(symbol), None) if currency_symbol(symbol) => Ok(CurrencySign { value, symbol, hex }),
1020 _ => Err(crate::messages::IWS0043.at(pos, format!("PICTURE SYMBOL {symbol:?} is not one character that can be a PICTURE currency symbol"))),
1021 }
1022 }
1023
1024 fn select(&mut self) -> R<FileDecl> {
1025 let pos = self.pos();
1026 let optional = self.accept_word("OPTIONAL");
1027 let name = self.name("a file name")?;
1028 let mut f = FileDecl {
1029 name,
1030 assign: String::new(),
1031 assign_item: None,
1032 organization: Organization::Sequential,
1033 access: Access::Sequential,
1034 record_key: None,
1035 label_records: Vec::new(),
1036 alternate_keys: Vec::new(),
1037 split_keys: Vec::new(),
1038 relative_key: None,
1039 optional,
1040 status: None,
1041 vsam_status: None,
1042 passwords: Vec::new(),
1043 recording: None,
1044 record_min: None,
1045 record_max: None,
1046 record_varying: false,
1047 record_depending: None,
1048 records: Vec::new(),
1049 reports: Vec::new(),
1050 linage: None,
1051 sort: false,
1052 external: false,
1053 global: false,
1054 declared_in: None,
1055 pos,
1056 };
1057 let mut delimiter = None;
1058 while !self.accept(&Tok::Period) {
1059 let clause = self.name("a SELECT clause or a period")?;
1060 match clause.as_str() {
1061 "ASSIGN" => {
1062 let using = self.accept_word("USING");
1063 if !using {
1064 self.accept_word("TO");
1065 }
1066 if using || self.accept_word("DYNAMIC") {
1067 let reference = self.reference()?;
1068 f.assign = reference.name.clone();
1069 f.assign_item = Some(AssignItem { reference, explicit: true, declared_if_missing: false });
1070 continue;
1071 }
1072 let disk = self.extended
1073 && self.is_word("DISK")
1074 && match self.tokens.get(self.at + 1).map(|t| &t.tok) {
1075 Some(Tok::Alnum(_)) => true,
1076 Some(Tok::Word(w)) => !SELECT_CLAUSES.contains(&w.as_str()),
1077 _ => false,
1078 };
1079 if disk {
1080 self.messages.push(crate::messages::IWX0039.at(self.pos(), "ASSIGN TO DISK (Micro Focus and GnuCOBOL; Enterprise COBOL's ASSIGN names a DD): DISK is the device, and what follows names the file"));
1081 self.at += 1;
1082 }
1083 let external = self.is_word("EXTERNAL") && matches!(self.tokens.get(self.at + 1).map(|t| &t.tok), Some(Tok::Word(_) | Tok::Alnum(_)));
1084 if external {
1085 self.at += 1;
1086 }
1087 let at = self.pos();
1088 let (target, word) = match self.peek().cloned() {
1089 Some(Tok::Word(w)) => (w, !external),
1090 Some(Tok::Alnum(w)) => (w, false),
1091 _ => return Err(self.error("a DD name after ASSIGN")),
1092 };
1093 self.at += 1;
1094 if word {
1095 f.assign_item = Some(AssignItem { reference: Ref { name: target.clone(), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos: at }, explicit: false, declared_if_missing: disk });
1096 }
1097 let target = target.to_ascii_uppercase();
1098 f.assign = target.rsplit('-').next().filter(|_| target.contains("-S-") || target.starts_with("S-") || target.starts_with("AS-")).unwrap_or(&target).to_owned();
1099 while match self.peek() {
1101 Some(Tok::Alnum(_)) => true,
1102 Some(Tok::Word(w)) => !SELECT_CLAUSES.contains(&w.as_str()),
1103 _ => false,
1104 } {
1105 self.at += 1;
1106 }
1107 }
1108 "PASSWORD" => {
1109 self.accept_word("IS");
1110 f.passwords.push(self.reference()?);
1111 }
1112 "RECORD" if self.is_word("DELIMITER") => {
1114 delimiter = Some(self.pos());
1115 self.at += 1;
1116 self.accept_word("IS");
1117 self.name("STANDARD-1 or an assignment-name")?;
1118 }
1119 "RECORD" if !self.is_word("SEQUENTIAL") => {
1120 self.accept_word("KEY");
1121 self.accept_word("IS");
1122 let key = self.reference()?;
1123 self.split_key(&mut f, &key)?;
1124 f.record_key = Some(key);
1125 }
1126 "RELATIVE" if self.is_word("KEY") || self.is_word("IS") || self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) => {
1127 self.accept_word("KEY");
1128 self.accept_word("IS");
1129 f.relative_key = Some(self.reference()?);
1130 }
1131 "ALTERNATE" => {
1132 self.accept_word("RECORD");
1133 self.accept_word("KEY");
1134 self.accept_word("IS");
1135 let key = self.reference()?;
1136 self.split_key(&mut f, &key)?;
1137 let duplicates = self.accept_word("WITH") | self.is_word("DUPLICATES");
1138 if duplicates {
1139 self.expect_word("DUPLICATES")?;
1140 }
1141 f.alternate_keys.push((key, duplicates));
1142 }
1143 "ORGANIZATION" | "LINE" | "RECORD" | "SEQUENTIAL" | "INDEXED" | "RELATIVE" => {
1144 if clause == "ORGANIZATION" {
1145 self.accept_word("IS");
1146 }
1147 let first = if clause == "ORGANIZATION" { self.name("an organization")? } else { clause.clone() };
1148 f.organization = match first.as_str() {
1149 "LINE" => {
1150 self.expect_word("SEQUENTIAL")?;
1151 Organization::LineSequential
1152 }
1153 "RECORD" => {
1154 self.expect_word("SEQUENTIAL")?;
1155 Organization::Sequential
1156 }
1157 "SEQUENTIAL" => Organization::Sequential,
1158 "INDEXED" => Organization::Indexed,
1159 "RELATIVE" => Organization::Relative,
1160 other => return Err(crate::messages::IWR0007.at(self.pos(), format!("ORGANIZATION {other} is not supported yet"))),
1161 };
1162 }
1163 "ACCESS" => {
1164 self.accept_word("MODE");
1165 self.accept_word("IS");
1166 f.access = match self.name("an access mode")?.as_str() {
1167 "SEQUENTIAL" => Access::Sequential,
1168 "RANDOM" => Access::Random,
1169 "DYNAMIC" => Access::Dynamic,
1170 other => return Err(self.error(format!("ACCESS MODE {other} is not an access mode"))),
1171 };
1172 }
1173 "FILE" | "STATUS" => {
1174 if clause == "FILE" {
1175 self.expect_word("STATUS")?;
1176 }
1177 self.accept_word("IS");
1178 f.status = Some(self.reference()?);
1179 if self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
1180 f.vsam_status = Some(self.reference()?);
1181 }
1182 }
1183 "RESERVE" | "PADDING" => {
1184 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
1185 self.at += 1;
1186 }
1187 }
1188 "LOCK" if self.extended => {
1189 let at = self.tokens[self.at - 1].pos;
1190 self.accept_word("MODE");
1191 self.accept_word("IS");
1192 let mode = self.name("MANUAL, AUTOMATIC or EXCLUSIVE")?;
1193 if !matches!(mode.as_str(), "MANUAL" | "AUTOMATIC" | "EXCLUSIVE") {
1194 return Err(self.error(format!("LOCK MODE {mode}: MANUAL, AUTOMATIC or EXCLUSIVE")));
1195 }
1196 if self.is_word("WITH") && self.word_at(1) == Some("LOCK") {
1197 self.at += 2;
1198 self.expect_word("ON")?;
1199 self.accept_word("MULTIPLE");
1200 self.accept_any(&["RECORD", "RECORDS"]);
1201 }
1202 self.locked(&format!("LOCK MODE {mode}"), at);
1203 }
1204 "SHARING" if self.extended => {
1205 let at = self.tokens[self.at - 1].pos;
1206 self.accept_word("WITH");
1207 let with = match self.accept_any(&["ALL", "NO", "READ"]).as_deref() {
1208 Some("ALL") => self.accept_word("OTHER").then_some("ALL OTHER"),
1209 Some("NO") => self.accept_word("OTHER").then_some("NO OTHER"),
1210 Some(_) => self.accept_word("ONLY").then_some("READ ONLY"),
1211 None => None,
1212 };
1213 let with = with.ok_or_else(|| self.error("ALL OTHER, NO OTHER or READ ONLY after SHARING WITH"))?;
1214 self.locked(&format!("SHARING WITH {with}"), at);
1215 }
1216 other => return Err(crate::messages::IWR0008.at(self.pos(), format!("{other} is not a SELECT clause ironwork for COBOL supports yet"))),
1217 }
1218 }
1219 if let Some(at) = delimiter.filter(|_| f.organization != Organization::Sequential) {
1220 return Err(crate::messages::IWS0044.at(at, format!("RECORD DELIMITER on {}: the clause is for a file of ORGANIZATION SEQUENTIAL", f.name)));
1221 }
1222 Ok(f)
1223 }
1224
1225 fn file_section(&mut self, files: &mut [FileDecl]) -> R<()> {
1228 while self.is_word("FD") || self.is_word("SD") {
1229 let indicator = self.name("FD or SD")?;
1230 let pos = self.pos();
1231 let name = self.name("a file name")?;
1232 let Some(index) = files.iter().position(|f| f.name == name) else {
1233 return Err(crate::messages::IWS0045.at(pos, format!("{indicator} {name} has no SELECT")));
1234 };
1235 files[index].sort = indicator == "SD";
1236 while !self.accept(&Tok::Period) {
1237 match self.name("an FD clause or a period")?.as_str() {
1238 "RECORDING" => {
1239 self.accept_word("MODE");
1240 self.accept_word("IS");
1241 let mode = self.name("F, V, U or S")?;
1242 files[index].recording = mode.chars().next();
1243 }
1244 "RECORD" => {
1245 self.accept_word("CONTAINS");
1246 self.accept_word("IS");
1247 let varying = self.accept_word("VARYING");
1248 if varying {
1249 files[index].recording.get_or_insert('V');
1250 self.accept_word("IN");
1251 self.accept_word("SIZE");
1252 self.accept_word("FROM");
1253 }
1254 let number = |p: &mut Self| -> R<Option<u32>> {
1255 match p.peek() {
1256 Some(Tok::Number(n)) => {
1257 let v = n.parse().map_err(|_| p.error("a record length"))?;
1258 p.at += 1;
1259 Ok(Some(v))
1260 }
1261 _ => Ok(None),
1262 }
1263 };
1264 let first = number(self)?;
1265 let second = if self.accept_word("TO") { number(self)? } else { None };
1266 if varying {
1267 files[index].record_varying = true;
1268 (files[index].record_min, files[index].record_max) = (first, second);
1269 } else if first.is_some() {
1270 files[index].record_min = first;
1271 files[index].record_max = second.or(first);
1272 }
1273 self.accept_word("CHARACTERS");
1274 if self.accept_word("DEPENDING") {
1275 self.accept_word("ON");
1276 files[index].record_depending = Some(self.reference()?);
1277 }
1278 }
1279 "REPORT" | "REPORTS" if files[index].sort => return Err(crate::messages::IWS0046.at(pos, format!("SD {name}: a sort or merge file takes no REPORT clause"))),
1280 "REPORT" | "REPORTS" => {
1281 let names = self.report_names()?;
1282 files[index].reports.extend(names);
1283 }
1284 "EXTERNAL" | "GLOBAL" if files[index].sort => return Err(crate::messages::IWS0047.at(pos, format!("SD {name}: a sort or merge file takes no EXTERNAL or GLOBAL clause"))),
1285 "EXTERNAL" => files[index].external = true,
1286 "GLOBAL" => files[index].global = true,
1287 "LABEL" => {
1288 self.accept_any(&["RECORD", "RECORDS"]);
1289 self.accept_any(&["IS", "ARE"]);
1290 while self.word().is_some_and(|w| !FD_WORDS.contains(&w)) {
1291 if self.accept_any(&["STANDARD", "OMITTED"]).is_none() {
1292 let r = self.reference()?;
1293 files[index].label_records.push(r);
1294 }
1295 }
1296 }
1297 "LINAGE" => {
1298 let linage = self.linage()?;
1299 if files[index].linage.is_some() {
1300 return Err(crate::messages::IWS0048.at(pos, format!("{indicator} {name}: LINAGE is given twice")));
1301 }
1302 files[index].linage = (indicator == "FD").then_some(linage);
1303 }
1304 _ => {
1305 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| FD_WORDS.contains(&w)) {
1306 self.at += 1;
1307 }
1308 }
1309 }
1310 }
1311 let mut records = self.data_entries()?;
1312 if let Some(e) = records.iter().find(|e| e.external) {
1313 return Err(crate::messages::IWS0049.at(e.pos, format!("{indicator} {name}: EXTERNAL goes on the FD, not on a record of the FILE SECTION")));
1314 }
1315 let (external, global) = (files[index].external, files[index].global);
1316 for e in &mut records {
1317 e.external = external;
1318 e.global |= global;
1319 }
1320 files[index].records = records;
1321 }
1322 Ok(())
1323 }
1324
1325 fn linage(&mut self) -> R<Linage> {
1327 self.accept_word("IS");
1328 let lines = self.linage_value("LINAGE")?;
1329 self.accept_word("LINES");
1330 let mut linage = Linage { lines, footing: None, top: None, bottom: None };
1331 loop {
1332 let start = self.at;
1333 self.accept_word("WITH");
1334 self.accept_word("LINES");
1335 self.accept_word("AT");
1336 let Some(phrase) = self.accept_any(&["FOOTING", "TOP", "BOTTOM"]) else {
1337 self.at = start;
1338 return Ok(linage);
1339 };
1340 if phrase == "FOOTING" {
1341 self.accept_word("AT");
1342 }
1343 let value = Some(self.linage_value(&phrase)?);
1344 let slot = match phrase.as_str() {
1345 "FOOTING" => &mut linage.footing,
1346 "TOP" => &mut linage.top,
1347 _ => &mut linage.bottom,
1348 };
1349 if slot.is_some() {
1350 return Err(self.error(format!("LINAGE: {phrase} is given twice")));
1351 }
1352 *slot = value;
1353 }
1354 }
1355
1356 fn linage_value(&mut self, phrase: &str) -> R<LinageValue> {
1358 match self.peek() {
1359 Some(Tok::Number(n)) if n.bytes().all(|b| b.is_ascii_digit()) => {
1360 let n = n.clone();
1361 self.at += 1;
1362 Ok(LinageValue::Integer(n))
1363 }
1364 Some(Tok::Number(n)) => Err(self.error(format!("LINAGE: {phrase} {n} is not an unsigned integer"))),
1365 Some(Tok::Word(_)) if self.starts_ref() => {
1366 let r = self.reference()?;
1367 if !r.subscripts.is_empty() || r.refmod.is_some() {
1368 return Err(crate::messages::IWS0050.at(r.pos, format!("LINAGE: {phrase} {} takes no subscript or reference modification", r.name)));
1369 }
1370 Ok(LinageValue::Data(r))
1371 }
1372 _ => Err(self.error(format!("an integer or a data-name after {phrase}"))),
1373 }
1374 }
1375
1376 fn data_exec(&mut self) -> R<bool> {
1378 let Some(Tok::Exec(text)) = self.peek().cloned() else { return Ok(false) };
1379 let block = self.exec_block(&text, self.pos());
1380 self.at += 1;
1381 self.accept(&Tok::Period);
1382 self.exec_declarations.push(block);
1383 Ok(true)
1384 }
1385
1386 fn translator_additions(&mut self, linkage: &mut Vec<DataEntry>, using: &mut Vec<Param>) -> R<()> {
1389 let defined = |l: &[DataEntry], n: &str| l.iter().any(|e| e.level == 1 && e.name.as_deref() == Some(n));
1390 let mut added = Vec::new();
1391 if !defined(linkage, "DFHEIBLK") {
1392 added.extend(system_entries("DFHEIBLK")?);
1393 }
1394 if !defined(linkage, "DFHCOMMAREA") {
1395 added.extend(system_text_entries(" 01 DFHCOMMAREA PIC X(1).\n")?);
1396 }
1397 linkage.splice(0..0, added);
1398 for (i, name) in ["DFHEIBLK", "DFHCOMMAREA"].into_iter().enumerate() {
1399 if using.get(i).map(|p| p.name.as_str()) != Some(name) {
1400 using.insert(i, Param { by_value: false, name: name.into() });
1401 }
1402 }
1403 Ok(())
1404 }
1405
1406 fn exec_block(&mut self, text: &str, pos: Pos) -> ExecBlock {
1407 let (kind_word, body) = text.split_once(' ').unwrap_or((text, ""));
1408 let kind = match kind_word.to_ascii_uppercase().as_str() {
1409 "SQL" => ExecKind::Sql,
1410 "CICS" => ExecKind::Cics,
1411 "DLI" => ExecKind::Dli,
1412 _ => ExecKind::Other,
1413 };
1414 let words: Vec<String> = body.split_whitespace().map(|w| w.to_ascii_uppercase()).collect();
1415 let word = |i: usize| words.get(i).map(String::as_str).unwrap_or("");
1416 let mut block = ExecBlock { kind, command: word(0).to_owned(), options: Vec::new(), host_variables: Vec::new(), sql: None, text: text.to_owned(), pos };
1417 match kind {
1418 ExecKind::Sql => {
1419 block.command = match (word(0), word(1), word(2)) {
1420 ("DECLARE", _, "CURSOR") => "DECLARE CURSOR".into(),
1421 ("DECLARE", _, "TABLE") => "DECLARE TABLE".into(),
1422 ("DECLARE", _, "STATEMENT") => "DECLARE STATEMENT".into(),
1423 ("BEGIN" | "END", "DECLARE", "SECTION") => format!("{} DECLARE SECTION", word(0)),
1424 (first, _, _) => first.into(),
1425 };
1426 let longest = (!self.extended).then_some(crate::lexer::USER_WORD_MAX);
1427 let statement = self.sql.cursors.resolve(crate::sql::parse_naming(body, pos, longest));
1428 if let crate::sql::Statement::Whenever { condition, action } = &statement {
1429 self.sql.whenever.set(*condition, action.clone());
1430 }
1431 block.host_variables = match &statement {
1432 crate::sql::Statement::Unsupported(_)
1433 | crate::sql::Statement::Connect { .. }
1434 | crate::sql::Statement::Malformed(_)
1435 | crate::sql::Statement::Declaration
1436 | crate::sql::Statement::DeclareUnsupported { .. } => host_variables(body, pos, longest),
1437 typed => typed.references().into_iter().cloned().collect(),
1438 };
1439 self.sql.blocks += 1;
1440 block.sql = Some(crate::sql::Sql { statement, ordinal: self.sql.blocks, whenever: self.sql.whenever.clone() });
1441 }
1442 ExecKind::Cics => {
1443 self.cics = true;
1444 block.options = cics_options(body);
1445 if let Some((first, None)) = block.options.first().cloned() {
1446 block.command = first.clone();
1447 block.options.remove(0);
1448 if let Some((second, None)) = block.options.first().cloned()
1449 && crate::system::cics_two_word(&first, &second)
1450 {
1451 block.command = format!("{first} {second}");
1452 block.options.remove(0);
1453 }
1454 }
1455 let labels = block.command.starts_with("HANDLE");
1456 for (name, arg) in &mut block.options {
1457 if let Some(ExecArg::Text(t)) = arg
1458 && (!labels || matches!(name.as_str(), "RESP" | "RESP2" | "PROGRAM"))
1459 && let Some(op) = operand_of(t, pos, self.extended)
1460 {
1461 *arg = Some(ExecArg::Operand(op));
1462 }
1463 }
1464 }
1465 ExecKind::Dli => {
1466 self.dli = true;
1467 let words: Vec<&str> = body.split_whitespace().collect();
1468 let rest = match crate::dli::command(&words) {
1469 Some((command, n)) => {
1470 block.command = command.name.to_owned();
1471 let mut rest = body.trim_start();
1472 for _ in 0..n {
1473 rest = rest.trim_start().split_once(char::is_whitespace).map_or("", |(_, r)| r);
1474 }
1475 rest
1476 }
1477 None => body.trim_start().split_once(char::is_whitespace).map_or("", |(_, r)| r),
1478 };
1479 block.options = cics_options(rest).into_iter().filter(|(name, arg)| !(name == "USING" && arg.is_none())).collect();
1480 for (name, arg) in &mut block.options {
1481 let Some(ExecArg::Text(t)) = arg else { continue };
1482 if name == "WHERE" {
1483 if let Ok(comparisons) = crate::dli::qualification(t) {
1484 for (_, _, value) in comparisons {
1485 if let Some(Operand::Ref(r)) = operand_of(&value, pos, self.extended) {
1486 block.host_variables.push(r);
1487 }
1488 }
1489 }
1490 continue;
1491 }
1492 let area = t.strip_prefix('(').and_then(|a| a.strip_suffix(')'));
1493 let data = match area {
1494 Some(area) if crate::dli::NAMED.contains(&name.as_str()) => area.trim(),
1495 _ if crate::dli::NAMED.contains(&name.as_str()) => continue,
1496 _ => t.as_str(),
1497 };
1498 if let Some(op) = operand_of(data, pos, self.extended) {
1499 *arg = Some(ExecArg::Operand(op));
1500 }
1501 }
1502 }
1503 _ => {}
1504 }
1505 block
1506 }
1507
1508 fn data_entries(&mut self) -> R<Vec<DataEntry>> {
1509 let mut entries = Vec::new();
1510 loop {
1511 if self.data_exec()? {
1512 continue;
1513 }
1514 let Some((level, pos)) = self.level_number()? else { break };
1515 entries.push(self.data_entry(level, pos)?);
1516 }
1517 Ok(entries)
1518 }
1519
1520 fn level_number(&mut self) -> R<Option<(u8, Pos)>> {
1521 let Some(Tok::Number(level)) = self.peek() else { return Ok(None) };
1522 let pos = self.pos();
1523 let level = level.parse().map_err(|_| self.error("a level number"))?;
1524 self.at += 1;
1525 Ok(Some((level, pos)))
1526 }
1527
1528 fn picture(&mut self) -> R<String> {
1530 self.accept_word("IS");
1531 match self.peek() {
1532 Some(Tok::Pic(p)) => {
1533 let p = p.clone();
1534 self.at += 1;
1535 Ok(p)
1536 }
1537 _ => Err(self.error("a PICTURE character-string")),
1538 }
1539 }
1540
1541 fn sign_clause(&mut self, first: &str) -> R<SignClause> {
1543 let side = if first == "SIGN" {
1544 self.accept_word("IS");
1545 self.name("LEADING or TRAILING")?
1546 } else {
1547 first.to_owned()
1548 };
1549 let position = match side.as_str() {
1550 "LEADING" => SignPosition::Leading,
1551 "TRAILING" => SignPosition::Trailing,
1552 _ => return Err(self.error("LEADING or TRAILING")),
1553 };
1554 let separate = self.accept_word("SEPARATE");
1555 if separate {
1556 self.accept_word("CHARACTER");
1557 }
1558 Ok(SignClause { position, separate })
1559 }
1560
1561 fn values_outside_condition(&mut self, level: u8) {
1565 let pos = self.tokens[self.at - 1].pos;
1566 self.messages.push(if self.extended {
1567 IWX0014.at(pos, "VALUES outside a level-88 entry (Micro Focus; Enterprise COBOL writes VALUE there): it is read as VALUE")
1568 } else {
1569 IWS0098.at(pos, format!("VALUES in a level-{level:02} entry: Enterprise COBOL writes VALUES only in a level-88 entry, and VALUE in any other; --compliance extended reads it as VALUE"))
1570 });
1571 }
1572
1573 fn blank_when_zero(&mut self) -> R<()> {
1575 self.accept_word("WHEN");
1576 if self.accept_any(&["ZERO", "ZEROS", "ZEROES"]).is_none() {
1577 return Err(self.error("ZERO after BLANK WHEN"));
1578 }
1579 Ok(())
1580 }
1581
1582 fn data_entry(&mut self, level: u8, pos: Pos) -> R<DataEntry> {
1583 let mut e = DataEntry {
1584 level,
1585 name: None,
1586 spelled: None,
1587 picture: None,
1588 usage: None,
1589 value: None,
1590 redefines: None,
1591 occurs: None,
1592 occurs_min: None,
1593 depending_on: None,
1594 sign: None,
1595 justified: false,
1596 sync: false,
1597 blank_when_zero: false,
1598 indexed_by: Vec::new(),
1599 keys: Vec::new(),
1600 condition_values: Vec::new(),
1601 false_value: None,
1602 renames: None,
1603 object_class: None,
1604 external: false,
1605 global: false,
1606 any_length: false,
1607 based: false,
1608 pos,
1609 };
1610 if let Some(w) = self.word()
1611 && !is_clause_word(w)
1612 {
1613 if w != "FILLER" {
1614 e.name = Some(w.to_owned());
1615 e.spelled = self.tokens[self.at].spelled.clone();
1616 }
1617 self.at += 1;
1618 }
1619 let mut floating = None;
1620 while !self.accept(&Tok::Period) {
1621 if self.is_word("PROCEDURE") && self.word_at(1) == Some("DIVISION") {
1623 let at = self.pos();
1624 self.messages.push(crate::messages::IWS0105.at(at, "a period was required before PROCEDURE DIVISION: one was assumed"));
1625 break;
1626 }
1627 let clause = self.name("a data description clause or a period")?;
1628 match clause.as_str() {
1629 "PIC" | "PICTURE" => e.picture = Some(self.picture()?),
1630 "USAGE" => {
1631 self.accept_word("IS");
1632 let w = self.name("a usage")?;
1633 if w == "OBJECT" {
1634 self.object_reference(&mut e)?;
1635 } else {
1636 let usage = usage_word(&w).ok_or_else(|| gnucobol_float(&w, pos).unwrap_or_else(|| crate::messages::IWR0004.at(pos, format!("USAGE {w} is not supported yet"))))?;
1637 e.usage = Some(self.signedness(usage));
1638 }
1639 }
1640 "OBJECT" => self.object_reference(&mut e)?,
1641 "VALUE" | "VALUES" => {
1642 if clause == "VALUES" && level != 88 {
1643 self.values_outside_condition(level);
1644 }
1645 self.accept_word("IS");
1646 self.accept_word("ARE");
1647 if level == 88 {
1648 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.is_word("WHEN") && !self.is_word("FALSE") {
1649 let low = self.literal()?;
1650 let high = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.literal()?) } else { None };
1651 e.condition_values.push((low, high));
1652 }
1653 if self.accept_word("WHEN") {
1654 self.accept_word("SET");
1655 self.accept_word("TO");
1656 self.expect_word("FALSE")?;
1657 self.accept_word("IS");
1658 e.false_value = Some(self.literal()?);
1659 } else if self.accept_word("FALSE") {
1660 self.accept_word("IS");
1661 e.false_value = Some(self.literal()?);
1662 }
1663 } else {
1664 let at = self.pos();
1665 e.value = Some(self.literal()?);
1666 if let Some(Literal::Number(mantissa)) = &e.value
1667 && let Some(fixed) = self.floating_point(&mantissa.clone(), at)?
1668 {
1669 e.value = Some(Literal::Number(fixed));
1670 floating = Some(at);
1671 }
1672 }
1673 }
1674 "REDEFINES" => e.redefines = Some(self.name("the item redefined")?),
1675 "RENAMES" => {
1676 let first = self.reference()?;
1677 let last = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.reference()?) } else { None };
1678 e.renames = Some((first, last));
1679 }
1680 "OCCURS" => {
1681 let count = |p: &mut Self| -> R<u32> {
1682 let n = match p.peek() {
1683 Some(Tok::Number(n)) => n.parse().map_err(|_| p.error("an OCCURS count"))?,
1684 _ => return Err(p.error("an OCCURS count")),
1685 };
1686 p.at += 1;
1687 Ok(n)
1688 };
1689 let mut most = count(self)?;
1690 let mut least = 1;
1691 if self.accept_word("TO") {
1692 least = most;
1693 most = count(self)?;
1694 }
1695 e.occurs = Some(most);
1696 self.accept_word("TIMES");
1697 if self.accept_word("DEPENDING") {
1698 self.accept_word("ON");
1699 e.depending_on = Some(self.reference()?);
1700 e.occurs_min = Some(least);
1701 }
1702 loop {
1703 if let Some(order) = self.accept_any(&["ASCENDING", "DESCENDING"]) {
1704 self.accept_word("KEY");
1705 self.accept_word("IS");
1706 while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "INDEXED" | "ASCENDING" | "DESCENDING")) {
1707 e.keys.push((order == "ASCENDING", self.reference()?));
1708 }
1709 } else if self.accept_word("INDEXED") {
1710 self.accept_word("BY");
1711 while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "ASCENDING" | "DESCENDING")) {
1712 e.indexed_by.push(self.name("an index name")?);
1713 }
1714 } else {
1715 break;
1716 }
1717 }
1718 }
1719 "SIGN" | "LEADING" | "TRAILING" => e.sign = Some(self.sign_clause(&clause)?),
1720 "JUSTIFIED" | "JUST" => {
1721 self.accept_word("RIGHT");
1722 e.justified = true;
1723 }
1724 "SYNC" | "SYNCHRONIZED" => {
1725 self.accept_any(&["LEFT", "RIGHT"]);
1726 e.sync = true;
1727 }
1728 "BLANK" => {
1729 self.blank_when_zero()?;
1730 e.blank_when_zero = true;
1731 }
1732 "EXTERNAL" => e.external = true,
1733 "GLOBAL" => e.global = true,
1734 "BASED" => {
1735 let at = self.tokens[self.at - 1].pos;
1736 if !self.extended {
1737 return Err(crate::messages::IWC0316.at(at, "BASED: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's; --compliance extended reads it"));
1738 }
1739 e.based = true;
1740 }
1741 "VOLATILE" => {}
1742 "ANY" if self.is_word("LENGTH") => {
1743 let at = self.tokens[self.at - 1].pos;
1744 self.at += 1;
1745 if !self.extended {
1746 return Err(crate::messages::IWC0308.at(at, "ANY LENGTH: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's; --compliance extended reads it"));
1747 }
1748 e.any_length = true;
1749 }
1750 "IS" if matches!(self.word(), Some("EXTERNAL" | "GLOBAL")) => {}
1751 other => match usage_word(other) {
1752 Some(u) => e.usage = Some(self.signedness(u)),
1753 None => {
1754 let pos = self.tokens[self.at - 1].pos;
1755 return Err(gnucobol_float(other, pos).unwrap_or_else(|| crate::messages::IWR0003.at(pos, format!("{other} is not a data description clause ironwork for COBOL supports yet"))));
1756 }
1757 },
1758 }
1759 }
1760 if let Some(at) = floating
1761 && (e.picture.is_some() || e.usage.is_some_and(|u| !matches!(u, Usage::Float1 | Usage::Float2)))
1762 {
1763 return Err(crate::messages::IWS0051.at(at, "a floating-point VALUE literal is for a COMP-1 or COMP-2 item, not a fixed-point one"));
1764 }
1765 Ok(e)
1766 }
1767
1768 fn lock_phrase(&mut self, phrase: &str, at: Pos) -> R<()> {
1771 if !self.extended {
1772 return Err(crate::messages::IWC0299.at(at, format!("{phrase}: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it")));
1773 }
1774 self.locked(phrase, at);
1775 Ok(())
1776 }
1777
1778 fn chaining(&mut self) -> R<Vec<Stmt>> {
1783 let at = self.pos();
1784 if !self.accept_word("CHAINING") {
1785 return Ok(Vec::new());
1786 }
1787 if !self.extended {
1788 return Err(crate::messages::IWC0320.at(at, "PROCEDURE DIVISION CHAINING: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's; --compliance extended reads it"));
1789 }
1790 self.messages.push(crate::messages::IWX0055.at(at, "PROCEDURE DIVISION CHAINING (GnuCOBOL and Micro Focus; Enterprise COBOL's main program takes its PARM through USING): each item takes the run's argument in its position, its bytes left-justified, where one is given"));
1791 let mut moves = Vec::new();
1792 for (k, param) in self.parameters()?.into_iter().enumerate() {
1793 let one = Expr::Operand(Operand::Literal(Literal::Number("1".into())));
1794 let bytes = Ref { name: param.name, qualifiers: Vec::new(), subscripts: Vec::new(), refmod: Some(RefMod { start: Box::new(one), length: None }), pos: at };
1795 let position = Expr::Operand(Operand::Literal(Literal::Number((k + 1).to_string())));
1796 let argument = FunctionCall { name: "CHAINING ARGUMENT".into(), args: vec![position, Expr::Operand(Operand::Ref(bytes.clone()))], modifier: None, refmod: None, all_subscripts: Vec::new(), pos: at };
1797 moves.push(Stmt::Move { from: Operand::Function(argument), to: vec![bytes], pos: at });
1798 }
1799 Ok(moves)
1800 }
1801
1802 fn returning_code(&mut self, verb: &str) -> R<()> {
1806 let at = self.pos();
1807 let Some(word) = self.accept_any(&["RETURNING", "GIVING"]) else { return Ok(()) };
1808 if !self.extended {
1809 return Err(crate::messages::IWC0306.at(at, format!("{verb} {word}: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's, where a MOVE to RETURN-CODE comes first; --compliance extended reads it")));
1810 }
1811 let from = self.operand()?;
1812 self.messages.push(crate::messages::IWX0033.at(at, format!("{verb} {word} (GnuCOBOL and Micro Focus; Enterprise COBOL moves the value to RETURN-CODE first): the value is moved to RETURN-CODE, then {verb} ends the program")));
1813 let to = Ref { name: "RETURN-CODE".into(), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos: at };
1814 self.before.push(Stmt::Move { from, to: vec![to], pos: at });
1815 Ok(())
1816 }
1817
1818 fn omitted(&mut self, r: &Ref, negated: bool) -> R<Cond> {
1822 let at = self.pos();
1823 self.at += 1;
1824 let written = format!("{}{} OMITTED", r.name, if negated { " NOT" } else { "" });
1825 if !self.extended {
1826 return Err(crate::messages::IWC0307.at(at, format!("{written}: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's, which writes ADDRESS OF {} = NULL; --compliance extended reads it", r.name)));
1827 }
1828 self.messages.push(crate::messages::IWX0034.at(at, format!("{written} (GnuCOBOL and Micro Focus; Enterprise COBOL writes ADDRESS OF {0} = NULL): it is read as ADDRESS OF {0} = NULL, true when the caller passed OMITTED or no argument there", r.name)));
1829 Ok(Cond::Rel(Expr::Operand(Operand::AddressOf(r.clone())), RelOp::Eq, Expr::Operand(Operand::Literal(Literal::Figurative(Figurative::Null)))))
1830 }
1831
1832 fn split_key(&mut self, f: &mut FileDecl, key: &Ref) -> R<()> {
1835 if self.peek() != Some(&Tok::Eq) {
1836 return Ok(());
1837 }
1838 let at = self.pos();
1839 self.at += 1;
1840 if !self.extended {
1841 return Err(crate::messages::IWC0311.at(at, format!("KEY IS {} = ...: Micro Focus's split key, not Enterprise COBOL's; --compliance extended reads it", key.name)));
1842 }
1843 let mut pieces = Vec::new();
1844 while self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w) || matches!(w, "WITH" | "DUPLICATES" | "SOURCE")) {
1845 pieces.push(self.reference()?);
1846 }
1847 if pieces.is_empty() {
1848 return Err(self.error("the data items a split key joins"));
1849 }
1850 self.messages.push(crate::messages::IWX0040.at(at, format!("KEY IS {} = ... (Micro Focus; Enterprise COBOL's key is one data item): the key joins {} items of the record, in the order written", key.name, pieces.len())));
1851 f.split_keys.push((key.name.clone(), pieces));
1852 Ok(())
1853 }
1854
1855 fn locked(&mut self, phrase: &str, at: Pos) {
1856 self.messages.push(crate::messages::IWX0022.at(at, format!("{phrase} (Micro Focus and GnuCOBOL; Enterprise COBOL has no record locks of its own): the run unit is the file's only user, so nothing it locks waits and the phrase changes nothing")));
1857 }
1858
1859 fn read_lock(&mut self) -> R<()> {
1861 let at = self.pos();
1862 let phrase = if self.is_word("WITH") && matches!(self.word_at(1), Some("LOCK" | "NO" | "KEPT" | "WAIT")) {
1863 self.at += 1;
1864 match self.name("LOCK, NO LOCK, KEPT LOCK or WAIT")?.as_str() {
1865 "LOCK" => "READ ... WITH LOCK",
1866 "NO" => {
1867 self.expect_word("LOCK")?;
1868 "READ ... WITH NO LOCK"
1869 }
1870 "KEPT" => {
1871 self.expect_word("LOCK")?;
1872 "READ ... WITH KEPT LOCK"
1873 }
1874 _ => "READ ... WITH WAIT",
1875 }
1876 } else if self.is_word("IGNORING") && self.word_at(1) == Some("LOCK") {
1877 self.at += 2;
1878 "READ ... IGNORING LOCK"
1879 } else {
1880 return Ok(());
1881 };
1882 self.lock_phrase(phrase, at)
1883 }
1884
1885 fn write_lock(&mut self, verb: &str) -> R<()> {
1887 let at = self.pos();
1888 let no = match (self.word(), self.word_at(1), self.word_at(2)) {
1889 (Some("WITH"), Some("LOCK"), _) => false,
1890 (Some("WITH"), Some("NO"), Some("LOCK")) => true,
1891 _ => return Ok(()),
1892 };
1893 self.at += if no { 3 } else { 2 };
1894 self.lock_phrase(&format!("{verb} ... WITH {}LOCK", if no { "NO " } else { "" }), at)
1895 }
1896
1897 fn screen_section(&mut self) -> R<Vec<ScreenEntry>> {
1900 let mut entries = Vec::new();
1901 while let Some(Tok::Number(n)) = self.peek().cloned() {
1902 let pos = self.pos();
1903 let level = n.parse().map_err(|_| self.error("a level number"))?;
1904 self.at += 1;
1905 let mut e = ScreenEntry { level, pos, ..ScreenEntry::default() };
1906 if let Some(w) = self.word().map(str::to_owned)
1907 && !SCREEN_CLAUSES.contains(&w.as_str())
1908 {
1909 self.at += 1;
1910 e.name = (w != "FILLER").then_some(w);
1911 }
1912 while self.peek().is_some() && self.peek() != Some(&Tok::Period) {
1913 let at = self.pos();
1914 let clause = self.name("a screen description clause")?;
1915 match clause.as_str() {
1916 "LINE" | "COL" | "COLUMN" => {
1917 self.accept_word("NUMBER");
1918 self.accept_word("IS");
1919 let place = if self.accept(&Tok::Plus) || self.accept_word("PLUS") {
1920 ScreenPlace::Plus(self.screen_number()?)
1921 } else if self.accept(&Tok::Minus) || self.accept_word("MINUS") {
1922 ScreenPlace::Minus(self.screen_number()?)
1923 } else {
1924 ScreenPlace::At(self.screen_number()?)
1925 };
1926 if clause == "LINE" { e.line = Some(place) } else { e.column = Some(place) }
1927 }
1928 "VALUE" => {
1929 self.accept_word("IS");
1930 e.value = Some(self.literal()?);
1931 }
1932 "PIC" | "PICTURE" => e.picture = Some(self.picture()?),
1933 "FROM" => e.from = Some(self.operand()?),
1934 "TO" => e.to = Some(self.reference()?),
1935 "USING" => e.using = Some(self.reference()?),
1936 "BLANK" => match self.accept_any(&["SCREEN", "LINE", "WHEN"]).as_deref() {
1937 Some("SCREEN") => e.blank_screen = true,
1938 Some("LINE") => e.blank_line = true,
1939 Some(_) => {
1940 if self.accept_any(&["ZERO", "ZEROS", "ZEROES"]).is_none() {
1941 return Err(self.error("ZERO after BLANK WHEN"));
1942 }
1943 e.attributes.push("BLANK WHEN ZERO".into());
1944 }
1945 None => return Err(self.error("SCREEN, LINE or WHEN ZERO after BLANK")),
1946 },
1947 "ERASE" => match self.accept_any(&["EOL", "EOS", "SCREEN", "LINE"]).as_deref() {
1948 Some("EOL") => e.erase_eol = true,
1949 Some("SCREEN") => e.blank_screen = true,
1950 Some("LINE") => e.blank_line = true,
1951 _ => e.erase_eos = true,
1952 },
1953 "SECURE" | "NO-ECHO" => e.secure = true,
1954 "OCCURS" => return Err(crate::messages::IWR0057.at(at, "OCCURS in the SCREEN SECTION is not supported yet")),
1955 "JUSTIFIED" | "JUST" => {
1956 self.accept_word("RIGHT");
1957 e.attributes.push(clause);
1958 }
1959 "SIGN" => {
1960 self.accept_word("IS");
1961 self.accept_any(&["LEADING", "TRAILING"]);
1962 if self.accept_word("SEPARATE") {
1963 self.accept_word("CHARACTER");
1964 }
1965 e.attributes.push(clause);
1966 }
1967 "USAGE" => {
1968 self.accept_word("IS");
1969 self.expect_word("DISPLAY")?;
1970 }
1971 "PROMPT" => {
1972 if self.accept_word("CHARACTER") {
1973 self.accept_word("IS");
1974 self.literal()?;
1975 }
1976 e.attributes.push(clause);
1977 }
1978 "FOREGROUND-COLOR" | "FOREGROUND-COLOUR" | "BACKGROUND-COLOR" | "BACKGROUND-COLOUR" | "COLOR" | "COLOUR" | "SIZE" | "CONTROL" => {
1979 self.accept_word("IS");
1980 self.operand()?;
1981 e.attributes.push(clause);
1982 }
1983 other if SCREEN_ATTRIBUTES.contains(&other) => e.attributes.push(clause),
1984 other => return Err(crate::messages::IWS0001.at(at, format!("a screen description clause, found {other}"))),
1985 }
1986 }
1987 self.expect(&Tok::Period, "a period")?;
1988 entries.push(e);
1989 }
1990 Ok(entries)
1991 }
1992
1993 fn screen_number(&mut self) -> R<u32> {
1995 match self.peek().cloned() {
1996 Some(Tok::Number(n)) if n.parse::<u32>().is_ok() => {
1997 self.at += 1;
1998 Ok(n.parse().unwrap_or_default())
1999 }
2000 _ => Err(crate::messages::IWR0057.at(self.pos(), "a SCREEN SECTION LINE or COLUMN that is not an integer literal is not supported yet")),
2001 }
2002 }
2003
2004 fn screen_phrases(&mut self, mut phrases: Option<Box<ScreenPhrases>>) -> R<Option<Box<ScreenPhrases>>> {
2008 let start = self.pos();
2009 loop {
2010 if self.is_word("WITH") && self.word_at(1) != Some("NO") {
2011 self.at += 1;
2012 continue;
2013 }
2014 let Some(word) = self.word().map(str::to_owned) else { break };
2015 let positional = matches!(word.as_str(), "LINE" | "COL" | "COLUMN" | "POSITION");
2016 if !(positional || word == "AT" || SCREEN_ATTRIBUTES.contains(&word.as_str()) || matches!(word.as_str(), "ERASE" | "BLANK" | "UPDATE" | "SECURE")) {
2017 break;
2018 }
2019 let p = phrases.get_or_insert_with(|| Box::new(ScreenPhrases { pos: start, ..ScreenPhrases::default() }));
2020 self.at += 1;
2021 match word.as_str() {
2022 "AT" if !matches!(self.word(), Some("LINE" | "COL" | "COLUMN" | "POSITION")) => p.at = Some(ScreenAt::Combined(self.operand()?)),
2023 "AT" => {}
2024 "LINE" | "COL" | "COLUMN" | "POSITION" => {
2025 self.accept_word("NUMBER");
2026 let at = self.operand()?;
2027 let (mut line, mut column) = match p.at.take() {
2028 Some(ScreenAt::LineColumn { line, column }) => (line, column),
2029 _ => (None, None),
2030 };
2031 if word == "LINE" { line = Some(at) } else { column = Some(at) }
2032 p.at = Some(ScreenAt::LineColumn { line, column });
2033 }
2034 "ERASE" => match self.accept_any(&["EOL", "EOS", "SCREEN", "LINE"]).as_deref() {
2035 Some("EOL") => p.erase_eol = true,
2036 Some("SCREEN") => p.blank_screen = true,
2037 Some("LINE") => p.blank_line = true,
2038 _ => p.erase_eos = true,
2039 },
2040 "BLANK" => match self.accept_any(&["SCREEN", "LINE"]).as_deref() {
2041 Some("SCREEN") => p.blank_screen = true,
2042 Some(_) => p.blank_line = true,
2043 None => return Err(self.error("SCREEN or LINE after BLANK")),
2044 },
2045 "UPDATE" => p.update = true,
2046 "SECURE" => p.secure = true,
2047 "FOREGROUND-COLOR" | "FOREGROUND-COLOUR" | "BACKGROUND-COLOR" | "BACKGROUND-COLOUR" | "TIMEOUT" | "TIME-OUT" | "SIZE" | "CONTROL" | "SCROLL" => {
2048 self.accept_any(&["IS", "AFTER", "UP", "DOWN"]);
2049 let value = if self.starts_operand() { Some(self.operand()?) } else { None };
2050 p.attributes.push(match value {
2051 Some(Operand::Literal(Literal::Number(n))) => format!("{word} {n}"),
2052 _ => word,
2053 });
2054 }
2055 "PROMPT" => {
2056 if self.accept_word("CHARACTER") {
2057 self.accept_word("IS");
2058 self.operand()?;
2059 }
2060 p.attributes.push(word);
2061 }
2062 _ => p.attributes.push(word),
2063 }
2064 }
2065 Ok(phrases)
2066 }
2067
2068 fn signedness(&mut self, usage: Usage) -> Usage {
2070 match usage {
2071 Usage::BinaryChar { .. } if self.accept_word("UNSIGNED") => Usage::BinaryChar { signed: false },
2072 Usage::BinaryChar { .. } => {
2073 self.accept_word("SIGNED");
2074 usage
2075 }
2076 _ => usage,
2077 }
2078 }
2079
2080 fn floating_point(&mut self, mantissa: &str, at: Pos) -> R<Option<String>> {
2084 let end = at.col + mantissa.chars().count() as u32;
2085 let adjacent = |p: &Self, k: usize, col: u32| p.tokens.get(p.at + k).is_some_and(|t| t.pos.file == at.file && t.pos.line == at.line && t.pos.col == col);
2086 let Some(Tok::Word(word)) = self.peek().cloned() else { return Ok(None) };
2087 if !word.starts_with('E') || !mantissa.contains('.') || !adjacent(self, 0, end) {
2088 return Ok(None);
2089 }
2090 let (exponent, used) = match (word.as_str(), self.peek_at(1)) {
2091 ("E", Some(Tok::Number(n))) if n.starts_with(['+', '-']) && adjacent(self, 1, end + 1) => (n.clone(), 2),
2092 ("E", _) => return Ok(None),
2093 (_, _) => (word[1..].to_owned(), 1),
2094 };
2095 let digits = exponent.trim_start_matches(['+', '-']);
2096 if digits.is_empty() || digits.len() > 2 || exponent.len() > digits.len() + 1 || !digits.bytes().all(|b| b.is_ascii_digit()) {
2097 return Ok(None);
2098 }
2099 self.at += used;
2100 let written = format!("{mantissa}E{exponent}");
2101 if mantissa.bytes().filter(u8::is_ascii_digit).count() > 16 {
2102 return Err(crate::messages::IWS0052.at(at, format!("{written}: a floating-point literal's mantissa has at most 16 digits")));
2103 }
2104 let exponent: i32 = exponent.parse().map_err(|_| crate::messages::IWS0053.at(at, format!("{written}: not an exponent")))?;
2105 fixed_point(mantissa, exponent).map(Some).ok_or_else(|| crate::messages::IWR0005.at(at, format!("VALUE {written}: a floating-point VALUE of more than 31 digits in fixed point is not supported yet")))
2106 }
2107
2108 fn literal(&mut self) -> R<Literal> {
2109 let lit = match self.peek().cloned() {
2110 Some(Tok::Alnum(s)) => Literal::Alnum(s),
2111 Some(Tok::Hex(b)) => Literal::Hex(b),
2112 Some(Tok::National(s)) => Literal::National(s),
2113 Some(Tok::Dbcs(s)) => Literal::Dbcs(s),
2114 Some(Tok::Number(n)) => Literal::Number(n),
2115 Some(Tok::Word(w)) if w == "ALL" => {
2116 let at = self.pos();
2117 self.at += 1;
2118 let inner = self.literal()?;
2119 let written = match &inner {
2121 Literal::Number(n) => Some(format!("ALL {n}")),
2122 Literal::All(_) => Some("ALL ALL".to_owned()),
2123 _ => None,
2124 };
2125 if let Some(written) = written {
2126 self.messages.push(crate::messages::IWS0054.at(at, format!("{written}: the literal after ALL is alphanumeric, national or a figurative constant other than ALL")));
2127 }
2128 return Ok(Literal::All(Box::new(inner)));
2129 }
2130 Some(Tok::Word(w)) => Literal::Figurative(figurative(&w).ok_or_else(|| self.error("a literal"))?),
2131 _ => return Err(self.error("a literal")),
2132 };
2133 self.at += 1;
2134 Ok(lit)
2135 }
2136
2137 fn unreserved_terminator(&mut self, word: &str, verb: &str) {
2141 let pos = self.pos();
2142 if !self.accept_word(word) {
2143 return;
2144 }
2145 self.messages.push(if self.extended {
2146 IWX0013.at(pos, format!("{word} (Micro Focus and GnuCOBOL; Enterprise COBOL does not reserve the word): it ends the {verb} statement"))
2147 } else {
2148 IWS0097.at(pos, format!("{word}: Micro Focus's and GnuCOBOL's scope terminator, a word Enterprise COBOL does not reserve; --compliance extended reads it"))
2149 });
2150 }
2151
2152 fn paragraph_header(&self) -> bool {
2155 self.tokens.get(self.at).is_some_and(|t| t.area_a && (matches!(&t.tok, Tok::Word(w) if !rt::reserved_words::is_reserved(w)) || digits(&t.tok))) && self.peek_at(1) == Some(&Tok::Period)
2156 }
2157
2158 fn section_header(&self) -> bool {
2159 self.tokens.get(self.at).is_some_and(|t| matches!(t.tok, Tok::Word(_)) || digits(&t.tok)) && self.word_at(0) != Some("EXIT") && self.word_at(1) == Some("SECTION")
2161 }
2162
2163 fn paragraphs(&mut self) -> R<Vec<Paragraph>> {
2164 let mut paragraphs = Vec::new();
2165 while self.peek().is_some() && !self.at_end_program() && !self.at_division(&["IDENTIFICATION", "ID"]) {
2166 if self.is_word("DECLARATIVES") {
2167 return Err(self.error("DECLARATIVES must begin the PROCEDURE DIVISION"));
2168 }
2169 self.procedure_item(&mut paragraphs)?;
2170 }
2171 Ok(paragraphs)
2172 }
2173
2174 fn procedure_item(&mut self, paragraphs: &mut Vec<Paragraph>) -> R<bool> {
2176 if self.section_header() {
2177 let pos = self.pos();
2178 let name = self.procedure_word("a section name")?;
2179 self.at += 1;
2180 let mut priority = 0;
2181 if let Some(Tok::Number(n)) = self.peek() {
2182 priority = n.trim_start_matches('+').parse().ok().filter(|p| *p <= 99).ok_or_else(|| self.error("a priority-number from 0 to 99"))?;
2183 self.at += 1;
2184 }
2185 self.expect(&Tok::Period, "a period after the section header")?;
2186 paragraphs.push(Paragraph { section: Some(name.clone()), name, statements: Vec::new(), is_section: true, priority, pos });
2187 return Ok(true);
2188 }
2189 if self.paragraph_header() {
2190 let pos = self.pos();
2191 let name = self.procedure_word("a paragraph name")?;
2192 self.at += 1;
2193 let (section, priority) = paragraphs.last().map_or((None, 0), |p| (p.section.clone(), p.priority));
2194 paragraphs.push(Paragraph { name, statements: Vec::new(), section, is_section: false, priority, pos });
2195 return Ok(false);
2196 }
2197 if self.accept(&Tok::Period) {
2198 if let Some(p) = paragraphs.last_mut()
2199 && p.statements.last().is_some_and(|s| *s != Stmt::SentenceEnd)
2200 {
2201 p.statements.push(Stmt::SentenceEnd);
2202 }
2203 return Ok(false);
2204 }
2205 let start = self.at;
2206 if self.stray_terminator() {
2207 return Ok(false);
2208 }
2209 let block = self.block(&[])?;
2210 if block.is_empty() {
2211 return Err(self.error("a statement"));
2212 }
2213 self.statement_words_in_area_a(start);
2214 if paragraphs.is_empty() {
2215 paragraphs.push(Paragraph { name: String::new(), statements: Vec::new(), section: None, is_section: false, priority: 0, pos: self.pos() });
2216 }
2217 paragraphs.last_mut().unwrap().statements.extend(block);
2218 Ok(false)
2219 }
2220
2221 fn statement_words_in_area_a(&mut self, start: usize) {
2225 for token in &self.tokens[start..self.at] {
2226 let Token { tok: Tok::Word(word), pos, area_a: true, .. } = token else { continue };
2227 self.messages.push(if self.extended {
2228 IWX0017.at(*pos, format!("a statement in Area A (Micro Focus and GnuCOBOL; Enterprise COBOL puts statements in Area B): {word} is read as though it began in Area B"))
2229 } else {
2230 IWS0100.at(*pos, format!("{word} begins in Area A, where Enterprise COBOL puts no statement: it is read as though it began in Area B"))
2231 });
2232 }
2233 }
2234
2235 fn block(&mut self, stops: &[&str]) -> R<Vec<Stmt>> {
2237 let depth = self.open.len();
2238 self.open.extend(stops.iter().map(|s| s.to_string()));
2239 let body = self.block_within(stops);
2240 self.open.truncate(depth);
2241 body
2242 }
2243
2244 fn block_within(&mut self, stops: &[&str]) -> R<Vec<Stmt>> {
2245 let mut out = Vec::new();
2246 while let Some(tok) = self.peek() {
2247 if *tok == Tok::Period || self.paragraph_header() || self.section_header() || self.word().is_some_and(|w| stops.contains(&w)) {
2248 break;
2249 }
2250 if self.stray_terminator() {
2251 continue;
2252 }
2253 if let Some(Tok::Exec(text)) = self.peek().cloned() {
2254 let block = self.exec_block(&text, self.pos());
2255 self.at += 1;
2256 out.push(Stmt::Exec(Box::new(block)));
2257 continue;
2258 }
2259 if !self.word().is_some_and(|w| self.is_verb(w) || self.extended && w == "UNLOCK") && !self.at_enable_or_disable() {
2260 break;
2261 }
2262 let statement = self.statement()?;
2263 out.append(&mut self.before);
2264 out.push(statement);
2265 }
2266 Ok(out)
2267 }
2268
2269 fn stray_terminator(&mut self) -> bool {
2272 let Some(word) = self.word().map(str::to_owned) else { return false };
2273 if !SCOPE_TERMINATORS.contains(&word.as_str()) || self.open.contains(&word) {
2274 return false;
2275 }
2276 let pos = self.pos();
2277 self.messages.push(IWS0104.at(pos, format!("{word}: an explicit scope terminator with no verb open for it; it was discarded")));
2278 self.at += 1;
2279 true
2280 }
2281
2282 fn statement(&mut self) -> R<Stmt> {
2283 let pos = self.pos();
2284 let verb = self.name("a statement")?;
2285 Ok(match verb.as_str() {
2286 "MOVE" => {
2287 if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() {
2288 return self.corresponding(CorrespondingVerb::Move, pos);
2289 }
2290 let from = self.operand()?;
2291 self.expect_word("TO")?;
2292 let to = self.refs()?;
2293 Stmt::Move { from, to, pos }
2294 }
2295 "COMPUTE" => {
2296 let targets = self.targets()?;
2297 if !self.accept(&Tok::Eq) {
2298 self.expect_word("EQUAL")?;
2299 }
2300 let expr = self.expr()?;
2301 let size_error = self.size_error()?;
2302 self.accept_word("END-COMPUTE");
2303 Stmt::Compute { targets, expr, size_error, pos }
2304 }
2305 "ADD" if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() => self.corresponding(CorrespondingVerb::Add, pos)?,
2306 "SUBTRACT" if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() => self.corresponding(CorrespondingVerb::Subtract, pos)?,
2307 "ADD" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" => Stmt::Arith(Box::new(self.arith(&verb, pos)?)),
2308 "IF" => {
2309 let cond = self.cond()?;
2310 self.accept_word("THEN");
2311 let then = self.block(&["ELSE", "END-IF"])?;
2312 let otherwise = if self.accept_word("ELSE") { self.block(&["END-IF"])? } else { Vec::new() };
2313 self.accept_word("END-IF");
2314 Stmt::If { cond, then, otherwise, pos }
2315 }
2316 "PERFORM" => self.perform(pos)?,
2317 "DISPLAY" => {
2318 let mut items = Vec::new();
2319 let no_advancing_ahead = |p: &Self| p.is_word("NO") && p.word_at(1) == Some("ADVANCING");
2320 while self.starts_operand() && !no_advancing_ahead(self) && !matches!(self.word(), Some("LINE" | "POSITION")) {
2322 items.push(self.operand()?);
2323 }
2324 let mut screen = self.screen_phrases(None)?;
2325 let mut upon = if self.accept_word("UPON") {
2326 let name = self.name("a mnemonic name")?;
2327 let device = self.mnemonics.iter().find(|(m, _)| *m == name).map_or_else(|| name.clone(), |(_, e)| e.clone());
2328 Some(Upon { name, device })
2329 } else {
2330 None
2331 };
2332 if upon.as_ref().is_some_and(|u| CRT_DEVICES.contains(&u.device.as_str())) {
2333 upon = None;
2334 screen.get_or_insert_with(|| Box::new(ScreenPhrases { pos, ..ScreenPhrases::default() }));
2335 }
2336 screen = self.screen_phrases(screen)?;
2337 let no_advancing = self.accept_word("WITH") | no_advancing_ahead(self);
2338 if no_advancing {
2339 self.expect_word("NO")?;
2340 self.expect_word("ADVANCING")?;
2341 if self.is_word("UPON") {
2342 return Err(self.error("the end of DISPLAY: Enterprise COBOL takes UPON before WITH NO ADVANCING"));
2343 }
2344 }
2345 self.unreserved_terminator("END-DISPLAY", "DISPLAY");
2346 Stmt::Display { items, upon, no_advancing, screen, pos }
2347 }
2348 "INITIALIZE" => self.initialize(pos)?,
2349 "INITIALISE" => {
2350 self.messages.push(crate::messages::IWX0052.at(pos, "INITIALISE (Micro Focus and GnuCOBOL; Enterprise COBOL spells it INITIALIZE): it is read as INITIALIZE"));
2351 self.initialize(pos)?
2352 }
2353 "ALLOCATE" => self.allocate(pos)?,
2354 "FREE" => self.free(pos)?,
2355 "CALL" => Stmt::Call(Box::new(self.call(pos)?)),
2356 "INVOKE" => Stmt::Invoke(Box::new(self.invoke(pos)?)),
2357 "JSON" if self.accept_word("GENERATE") => Stmt::JsonGenerate(Box::new(self.json_generate(pos)?)),
2358 "JSON" if self.accept_word("PARSE") => Stmt::JsonParse(Box::new(self.json_parse(pos)?)),
2359 "XML" if self.accept_word("PARSE") => Stmt::XmlParse(Box::new(self.xml_parse(pos)?)),
2360 "XML" if self.accept_word("GENERATE") => Stmt::XmlGenerate(Box::new(self.xml_generate(pos)?)),
2361 "CANCEL" => {
2362 let mut targets = Vec::new();
2363 while self.starts_operand() {
2364 targets.push(self.operand()?);
2365 }
2366 if targets.is_empty() {
2367 return Err(self.error("a program to CANCEL"));
2368 }
2369 Stmt::Cancel { targets, pos }
2370 }
2371 "SET" if self.is_word("ENVIRONMENT") => {
2372 self.at += 1;
2373 let name = self.operand()?;
2374 self.expect_word("TO")?;
2375 let value = self.operand()?;
2376 self.before.push(environment_display(name, "SET ENVIRONMENT", "ENVIRONMENT-NAME", pos));
2377 environment_display(value, "SET ENVIRONMENT", "ENVIRONMENT-VALUE", pos)
2378 }
2379 "SET" => Stmt::Set { set: self.set()?, pos },
2380 "STRING" => Stmt::String(Box::new(self.string(pos)?)),
2381 "UNSTRING" => Stmt::Unstring(Box::new(self.unstring(pos)?)),
2382 "INSPECT" => Stmt::Inspect(Box::new(self.inspect(pos)?)),
2383 "SEARCH" => Stmt::Search(Box::new(self.search(pos)?)),
2384 "SORT" | "MERGE" | "RELEASE" | "RETURN" => Stmt::Sorting(Box::new(self.sorting(&verb, pos)?)),
2385 "NEXT" => {
2386 self.expect_word("SENTENCE")?;
2387 Stmt::NextSentence
2388 }
2389 "ACCEPT" => {
2390 let target = self.reference()?;
2391 if let Some(refused) = self.accept_message_count(pos) {
2392 return Ok(refused);
2393 }
2394 if let Some(size) = self.screen_size() {
2395 self.unreserved_terminator("END-ACCEPT", "ACCEPT");
2396 return Ok(Stmt::Move { from: Operand::Literal(Literal::Number(size.into())), to: vec![target], pos });
2397 }
2398 let mut screen = None;
2399 if self.is_word("FROM") && self.word_at(1).is_some_and(|w| CRT_DEVICES.contains(&w)) {
2400 self.at += 2;
2401 screen = Some(Box::new(ScreenPhrases { pos, ..ScreenPhrases::default() }));
2402 }
2403 let screen = self.screen_phrases(screen)?;
2404 let mut named = None;
2405 if self.is_word("FROM") && self.word_at(1) == Some("ENVIRONMENT") {
2406 self.at += 2;
2407 named = Some(environment_display(self.operand()?, "ENVIRONMENT", "ENVIRONMENT-NAME", pos));
2408 }
2409 let from = if named.is_some() {
2410 AcceptFrom::EnvironmentValue
2411 } else if screen.is_none() && self.accept_word("FROM") {
2412 let at = self.pos();
2413 match self.name("SYSIN, SYSIPT, CONSOLE, a mnemonic-name for one, DATE, DAY, DAY-OF-WEEK or TIME")?.as_str() {
2414 "DATE" => AcceptFrom::Date { four_digit_year: self.accept_word("YYYYMMDD") },
2415 "DAY" => AcceptFrom::Day { four_digit_year: self.accept_word("YYYYDDD") },
2416 "DAY-OF-WEEK" => AcceptFrom::DayOfWeek,
2417 "TIME" => AcceptFrom::Time,
2418 "COMMAND-LINE" => AcceptFrom::CommandLine,
2419 "ARGUMENT-NUMBER" => AcceptFrom::ArgumentNumber,
2420 "ARGUMENT-VALUE" => AcceptFrom::ArgumentValue,
2421 "ENVIRONMENT-VALUE" => AcceptFrom::EnvironmentValue,
2422 name => {
2423 self.accept_device(name, at)?;
2424 AcceptFrom::Sysin
2425 }
2426 }
2427 } else {
2428 AcceptFrom::Sysin
2429 };
2430 let [exception] = self.on_phrases(&["ON", "EXCEPTION"], &["END-ACCEPT"], |p| {
2431 p.accept_word("ON");
2432 p.expect_word("EXCEPTION")?;
2433 Ok(0)
2434 })?;
2435 self.unreserved_terminator("END-ACCEPT", "ACCEPT");
2436 self.before.extend(named);
2437 Stmt::Accept { target, from, exception, screen, pos }
2438 }
2439 "OPEN" => {
2440 let mut files = Vec::new();
2441 while let Some(mode) = self.accept_any(&["INPUT", "OUTPUT", "EXTEND", "I-O"]) {
2442 let mode = match mode.as_str() {
2443 "INPUT" => OpenMode::Input,
2444 "OUTPUT" => OpenMode::Output,
2445 "EXTEND" => OpenMode::Extend,
2446 _ => OpenMode::InputOutput,
2447 };
2448 while self.starts_ref() {
2449 files.push((mode, self.name("a file name")?));
2450 self.accept_any(&["REVERSED"]);
2451 if self.is_word("WITH") && self.word_at(1) == Some("LOCK") {
2452 let at = self.pos();
2453 self.at += 2;
2454 self.lock_phrase("OPEN ... WITH LOCK", at)?;
2455 } else if self.accept_word("WITH") || self.is_word("NO") {
2456 self.expect_word("NO")?;
2457 self.expect_word("REWIND")?;
2458 }
2459 }
2460 }
2461 if files.is_empty() {
2462 return Err(self.error("INPUT, OUTPUT, EXTEND or I-O and a file"));
2463 }
2464 Stmt::Open { files, pos }
2465 }
2466 "CLOSE" => {
2467 let mut files = Vec::new();
2468 while self.starts_ref() {
2469 let name = self.name("a file name")?;
2470 let closing = if self.accept_any(&["REEL", "UNIT"]).is_some() {
2471 if self.accept_word("FOR") {
2472 self.expect_word("REMOVAL")?;
2473 } else if self.accept_word("WITH") || self.is_word("NO") {
2474 self.expect_word("NO")?;
2475 self.expect_word("REWIND")?;
2476 }
2477 Some(Closing::Volume)
2478 } else {
2479 self.accept_word("WITH");
2480 if self.accept_word("LOCK") {
2481 Some(Closing::Lock)
2482 } else if self.accept_word("NO") {
2483 self.expect_word("REWIND")?;
2484 Some(Closing::NoRewind)
2485 } else {
2486 None
2487 }
2488 };
2489 files.push((name, closing));
2490 }
2491 Stmt::Close { files, pos }
2492 }
2493 "READ" => {
2494 let file = self.name("a file name")?;
2495 let previous = self.accept_word("PREVIOUS");
2496 let next = previous || self.accept_word("NEXT");
2497 self.accept_word("RECORD");
2498 let into = if self.accept_word("INTO") { Some(self.reference()?) } else { None };
2499 self.read_lock()?;
2500 let key = if self.accept_word("KEY") {
2501 self.accept_word("IS");
2502 Some(self.reference()?)
2503 } else {
2504 None
2505 };
2506 self.read_lock()?;
2507 let [at_end, invalid] = self.on_phrases(&["AT", "END", "INVALID"], &["END-READ"], |p| {
2508 if p.accept_word("INVALID") {
2509 p.accept_word("KEY");
2510 return Ok(1);
2511 }
2512 p.accept_word("AT");
2513 p.expect_word("END").map(|()| 0)
2514 })?;
2515 self.accept_word("END-READ");
2516 Stmt::Read(Box::new(ReadStmt { file, next, previous, into, key, at_end, invalid, pos }))
2517 }
2518 "REWRITE" => {
2519 let record = self.reference()?;
2520 let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
2521 self.write_lock("REWRITE")?;
2522 let invalid = self.invalid_key("END-REWRITE")?;
2523 Stmt::Rewrite { record, from, invalid, pos }
2524 }
2525 "DELETE" if self.is_word("FILE") => {
2526 let at = self.pos();
2527 self.at += 1;
2528 if !self.extended {
2529 return Err(crate::messages::IWC0313.at(at, "DELETE FILE: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it"));
2530 }
2531 self.messages.push(crate::messages::IWX0043.at(at, "DELETE FILE (Micro Focus and GnuCOBOL; Enterprise COBOL has no such statement): each closed file's data set is removed"));
2532 let mut files = vec![self.name("a file name")?];
2533 while self.word().is_some_and(|w| !self.is_verb(w) && !w.starts_with("END-") && !PHRASE_WORDS.contains(&w)) {
2534 files.push(self.name("a file name")?);
2535 }
2536 Stmt::DeleteFile { files, pos }
2537 }
2538 "DELETE" => {
2539 let file = self.name("a file name")?;
2540 self.accept_word("RECORD");
2541 let invalid = self.invalid_key("END-DELETE")?;
2542 Stmt::Delete { file, invalid, pos }
2543 }
2544 "START" => {
2545 let file = self.name("a file name")?;
2546 let key = if self.accept_word("KEY") {
2547 self.accept_word("IS");
2548 let op = if self.accept_word("NOT") {
2549 match self.relop()? {
2550 Some(RelOp::Lt) => RelOp::Ge,
2551 Some(RelOp::Gt) => RelOp::Le,
2552 _ => return Err(self.error("NOT < or NOT > in START KEY")),
2553 }
2554 } else {
2555 self.relop()?.ok_or_else(|| self.error("a relation after START KEY"))?
2556 };
2557 Some((op, self.reference()?))
2558 } else {
2559 None
2560 };
2561 let invalid = self.invalid_key("END-START")?;
2562 Stmt::Start { file, key, invalid, pos }
2563 }
2564 "WRITE" => {
2565 let record = self.reference()?;
2566 let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
2567 self.write_lock("WRITE")?;
2568 let mut advancing = None;
2569 if let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
2570 let before = side == "BEFORE";
2571 self.accept_word("ADVANCING");
2572 let mnemonic = self.word().and_then(|w| self.mnemonics.iter().find(|(name, environment)| name == w && advancing_environment_name(environment))).cloned();
2573 advancing = Some(if self.accept_word("PAGE") {
2574 Advancing::Page { before }
2575 } else if let Some((name, environment)) = mnemonic {
2576 self.at += 1;
2577 Advancing::Mnemonic { before, name, environment }
2578 } else {
2579 let count = self.expr()?;
2580 self.accept_any(&["LINE", "LINES"]);
2581 Advancing::Lines { before, count }
2582 });
2583 }
2584 let eop_ahead = |p: &Self, i: usize| {
2585 let at = usize::from(p.word_at(i) == Some("AT"));
2586 matches!(p.word_at(i + at), Some("END-OF-PAGE" | "EOP"))
2587 };
2588 let [end_of_page, invalid] = self.phrases_opening(|p, i| eop_ahead(p, i) || p.word_at(i) == Some("INVALID"), &["END-WRITE"], |p| {
2589 if p.accept_word("INVALID") {
2590 p.accept_word("KEY");
2591 return Ok(1);
2592 }
2593 p.accept_word("AT");
2594 p.at += 1;
2595 Ok(0)
2596 })?;
2597 self.accept_word("END-WRITE");
2598 Stmt::Write { record, from, advancing, invalid, end_of_page, pos }
2599 }
2600 "GO" => {
2601 self.accept_word("TO");
2602 if self.peek() == Some(&Tok::Period) {
2603 return Ok(Stmt::GoTo { target: None, pos });
2604 }
2605 let target = self.proc_name()?;
2606 if !self.starts_proc_name() && !self.is_word("DEPENDING") {
2607 return Ok(Stmt::GoTo { target: Some(target), pos });
2608 }
2609 let mut targets = vec![target];
2610 while self.starts_proc_name() && !self.is_word("DEPENDING") {
2611 targets.push(self.proc_name()?);
2612 }
2613 self.expect_word("DEPENDING")?;
2614 self.accept_word("ON");
2615 Stmt::GoToDepending { targets, on: self.reference()?, pos }
2616 }
2617 "ALTER" => {
2618 let mut pairs = Vec::new();
2619 loop {
2620 let paragraph = self.proc_name()?;
2621 self.expect_word("TO")?;
2622 if self.accept_word("PROCEED") {
2623 self.expect_word("TO")?;
2624 }
2625 pairs.push((paragraph, self.proc_name()?));
2626 if !self.starts_proc_name() {
2627 break;
2628 }
2629 }
2630 Stmt::Alter { pairs, pos }
2631 }
2632 "ENTRY" => {
2633 let Some(Tok::Alnum(name)) = self.peek().cloned() else {
2634 return Err(self.error("an alphanumeric literal naming the entry point"));
2635 };
2636 self.at += 1;
2637 let using = if self.accept_word("USING") { self.parameters()? } else { Vec::new() };
2638 Stmt::Entry { name: name.to_ascii_uppercase(), using, pos }
2639 }
2640 "EVALUATE" => self.evaluate(pos)?,
2641 "INITIATE" | "GENERATE" | "TERMINATE" | "SUPPRESS" => Stmt::Report(Box::new(self.report_statement(&verb, pos)?)),
2642 "GOBACK" => {
2643 self.returning_code("GOBACK")?;
2644 Stmt::Goback { pos }
2645 }
2646 "STOP" if self.accept_word("RUN") => {
2647 self.returning_code("STOP RUN")?;
2648 Stmt::StopRun { pos }
2649 }
2650 "STOP" if self.starts_operand() && !self.starts_ref() => Stmt::Display { items: vec![self.operand()?], upon: Some(Upon { name: "CONSOLE".into(), device: "CONSOLE".into() }), no_advancing: false, screen: None, pos },
2652 "STOP" => return Err(self.error("RUN or a literal after STOP")),
2653 "CONTINUE" => Stmt::Continue { pos },
2654 "ENABLE" | "DISABLE" | "RECEIVE" | "SEND" | "PURGE" => self.communication_statement(&verb, pos)?,
2655 "UNLOCK" => {
2656 self.name("a file name")?;
2657 self.accept_any(&["RECORD", "RECORDS"]);
2658 self.lock_phrase("UNLOCK", pos)?;
2659 Stmt::Continue { pos }
2660 }
2661 "EXIT" if self.is_word("FUNCTION") => return Err(crate::messages::IWS0056.at(pos, "EXIT FUNCTION: Enterprise COBOL does not yet support the format 4 EXIT statement; GOBACK ends a user-defined function")),
2662 "EXIT" => match self.accept_any(&["PROGRAM", "PARAGRAPH", "SECTION", "PERFORM", "METHOD"]).as_deref() {
2663 Some("PROGRAM") => Stmt::ExitProgram { pos },
2664 Some("METHOD") => Stmt::ExitMethod { pos },
2665 Some("PARAGRAPH") => Stmt::Exit { kind: ExitKind::Paragraph, pos },
2666 Some("SECTION") => Stmt::Exit { kind: ExitKind::Section, pos },
2667 Some(_) if self.accept_word("CYCLE") => Stmt::Exit { kind: ExitKind::PerformCycle, pos },
2668 Some(_) => Stmt::Exit { kind: ExitKind::Perform, pos },
2669 None => Stmt::Exit { kind: ExitKind::Plain, pos },
2670 },
2671 other => return Err(crate::messages::IWR0002.at(pos, format!("{other} is not a statement ironwork for COBOL supports yet"))),
2672 })
2673 }
2674
2675 fn size_error(&mut self) -> R<Option<SizeError>> {
2676 let [h] = self.on_phrases(&["ON", "SIZE"], &["END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"], |p| {
2677 p.accept_word("ON");
2678 p.expect_word("SIZE")?;
2679 p.expect_word("ERROR").map(|()| 0)
2680 })?;
2681 Ok((h.on.is_some() || h.not_on.is_some()).then(|| SizeError { on: h.on.unwrap_or_default(), not_on: h.not_on.unwrap_or_default() }))
2682 }
2683
2684 fn corresponding(&mut self, verb: CorrespondingVerb, pos: Pos) -> R<Stmt> {
2687 let from = self.reference()?;
2688 self.expect_word(if verb == CorrespondingVerb::Subtract { "FROM" } else { "TO" })?;
2689 let to = self.reference()?;
2690 let (rounded, size_error) = match verb {
2691 CorrespondingVerb::Move => (false, None),
2692 _ => (self.accept_word("ROUNDED"), self.size_error()?),
2693 };
2694 if self.starts_ref() {
2695 return Err(crate::messages::IWS0057.at(self.pos(), "CORRESPONDING takes one receiving group"));
2696 }
2697 self.accept_any(&["END-ADD", "END-SUBTRACT"]);
2698 Ok(Stmt::Corresponding(Box::new(Corresponding { verb, from, to, rounded, size_error, pos })))
2699 }
2700
2701 fn targets(&mut self) -> R<Vec<Target>> {
2702 let mut out = Vec::new();
2703 while self.starts_ref() {
2704 let r = self.reference()?;
2705 out.push(Target { r, rounded: self.accept_word("ROUNDED") });
2706 }
2707 if out.is_empty() {
2708 return Err(self.error("a receiving item"));
2709 }
2710 Ok(out)
2711 }
2712
2713 fn refs(&mut self) -> R<Vec<Ref>> {
2714 let mut out = Vec::new();
2715 while self.starts_ref() {
2716 out.push(self.reference()?);
2717 }
2718 if out.is_empty() {
2719 return Err(self.error("a data name"));
2720 }
2721 Ok(out)
2722 }
2723
2724 fn operands_until(&mut self, stops: &[&str]) -> R<Vec<Expr>> {
2725 let mut out = Vec::new();
2726 while self.starts_operand() && !self.word().is_some_and(|w| stops.contains(&w)) {
2727 out.push(Expr::Operand(self.operand()?));
2728 }
2729 if out.is_empty() {
2730 return Err(self.error("an operand"));
2731 }
2732 Ok(out)
2733 }
2734
2735 fn arith(&mut self, verb: &str, pos: Pos) -> R<Arith> {
2736 let sum = |mut es: Vec<Expr>| {
2737 let first = es.remove(0);
2738 es.into_iter().fold(first, |acc, e| Expr::Bin(Box::new(acc), BinOp::Add, Box::new(e)))
2739 };
2740 let of = |t: &Target| Expr::Operand(Operand::Ref(t.r.clone()));
2741 let bin = |a: Expr, op: BinOp, b: Expr| Expr::Bin(Box::new(a), op, Box::new(b));
2742 let mut remainder = None;
2743 let (verb, computations) = match verb {
2744 "ADD" => {
2745 let addends = sum(self.operands_until(&["TO", "GIVING"])?);
2746 if self.accept_word("TO") {
2747 if self.words_ahead_include("GIVING") {
2748 let to = sum(self.operands_until(&["GIVING"])?);
2749 self.expect_word("GIVING")?;
2750 let targets = self.targets()?;
2751 let total = bin(addends, BinOp::Add, to);
2752 (ArithVerb::Add, targets.into_iter().map(|t| (t, total.clone())).collect())
2753 } else {
2754 let targets = self.targets()?;
2755 (ArithVerb::Add, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Add, addends.clone()))).collect())
2756 }
2757 } else {
2758 self.expect_word("GIVING")?;
2759 let targets = self.targets()?;
2760 (ArithVerb::Add, targets.into_iter().map(|t| (t, addends.clone())).collect())
2761 }
2762 }
2763 "SUBTRACT" => {
2764 let subtrahend = sum(self.operands_until(&["FROM"])?);
2765 self.expect_word("FROM")?;
2766 if self.words_ahead_include("GIVING") {
2767 let minuend = sum(self.operands_until(&["GIVING"])?);
2768 self.expect_word("GIVING")?;
2769 let targets = self.targets()?;
2770 let diff = bin(minuend, BinOp::Sub, subtrahend);
2771 (ArithVerb::Subtract, targets.into_iter().map(|t| (t, diff.clone())).collect())
2772 } else {
2773 let targets = self.targets()?;
2774 (ArithVerb::Subtract, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Sub, subtrahend.clone()))).collect())
2775 }
2776 }
2777 "MULTIPLY" => {
2778 let a = Expr::Operand(self.operand()?);
2779 self.expect_word("BY")?;
2780 if self.words_ahead_include("GIVING") {
2781 let b = Expr::Operand(self.operand()?);
2782 self.expect_word("GIVING")?;
2783 let targets = self.targets()?;
2784 (ArithVerb::Multiply, targets.into_iter().map(|t| (t, bin(a.clone(), BinOp::Mul, b.clone()))).collect())
2785 } else {
2786 let targets = self.targets()?;
2787 (ArithVerb::Multiply, targets.into_iter().map(|t| (t.clone(), bin(a.clone(), BinOp::Mul, of(&t)))).collect())
2788 }
2789 }
2790 _ => {
2791 let first = Expr::Operand(self.operand()?);
2792 let into = match self.accept_any(&["INTO", "BY"]).as_deref() {
2793 Some("INTO") => true,
2794 Some(_) => false,
2795 None => return Err(self.error("INTO or BY")),
2796 };
2797 if !into || self.words_ahead_include("GIVING") {
2798 let second = Expr::Operand(self.operand()?);
2799 let (dividend, divisor) = if into { (second, first) } else { (first, second) };
2800 self.expect_word("GIVING")?;
2801 let targets = self.targets()?;
2802 if self.accept_word("REMAINDER") {
2803 let r = self.reference()?;
2804 remainder = Some((Target { r, rounded: false }, dividend.clone(), divisor.clone()));
2805 }
2806 (ArithVerb::Divide, targets.into_iter().map(|t| (t, bin(dividend.clone(), BinOp::Div, divisor.clone()))).collect())
2807 } else {
2808 let targets = self.targets()?;
2809 (ArithVerb::Divide, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Div, first.clone()))).collect())
2810 }
2811 }
2812 };
2813 let size_error = self.size_error()?;
2814 self.accept_any(&["END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"]);
2815 Ok(Arith { verb, computations, remainder, size_error, pos })
2816 }
2817
2818 fn words_ahead_include(&self, word: &str) -> bool {
2820 self.tokens[self.at..]
2821 .iter()
2822 .take_while(|t| t.tok != Tok::Period && !matches!(&t.tok, Tok::Word(w) if self.is_verb(w) && w != word))
2823 .any(|t| matches!(&t.tok, Tok::Word(w) if w == word))
2824 }
2825
2826 fn perform(&mut self, pos: Pos) -> R<Stmt> {
2827 let named = (self.word().is_some_and(|w| !self.is_verb(w) && !PHRASE_WORDS.contains(&w) && w != "TEST") || self.peek().is_some_and(digits))
2828 && !self.times_ahead()
2829 && !(self.extended && self.is_word("FOREVER"));
2830 if named {
2831 let from = self.proc_name()?;
2832 let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.proc_name()?) } else { None };
2833 let repeat = self.repeat()?;
2834 return Ok(Stmt::PerformProc { from, thru, repeat, pos });
2835 }
2836 let repeat = self.repeat()?;
2837 if matches!(&repeat, Loop::Varying { after, .. } if !after.is_empty()) {
2838 if !self.extended {
2839 return Err(crate::messages::IWS0058.at(pos, "an inline PERFORM cannot have AFTER phrases: Enterprise COBOL takes them only when PERFORM names a procedure"));
2840 }
2841 self.messages.push(crate::messages::IWX0051.at(pos, "an inline PERFORM with AFTER phrases (GnuCOBOL and Micro Focus; Enterprise COBOL takes them only when PERFORM names a procedure): the body runs for each combination, the last AFTER varying fastest"));
2842 }
2843 let body = self.block(&["END-PERFORM"])?;
2844 self.expect_word("END-PERFORM")?;
2845 Ok(Stmt::PerformInline { body, repeat, pos })
2846 }
2847
2848 fn on_phrases<const N: usize>(&mut self, starts: &[&str], ends: &[&str], head: impl Fn(&mut Self) -> R<usize>) -> R<[Handlers; N]> {
2850 let opens = |p: &Self, i: usize| match p.word_at(i) {
2852 Some(w @ ("ON" | "AT")) if starts.contains(&w) => p.word_at(i + 1).is_some_and(|k| k != w && starts.contains(&k)),
2853 Some(w) => starts.contains(&w),
2854 None => false,
2855 };
2856 self.phrases_opening(opens, ends, head)
2857 }
2858
2859 fn phrases_opening<const N: usize>(&mut self, opens: impl Fn(&Self, usize) -> bool, ends: &[&str], head: impl Fn(&mut Self) -> R<usize>) -> R<[Handlers; N]> {
2861 let stops = [&["NOT"][..], ends].concat();
2862 let mut handlers = std::array::from_fn(|_| Handlers::default());
2863 loop {
2864 let negated = self.is_word("NOT") && opens(self, 1);
2865 if !negated && !opens(self, 0) {
2866 return Ok(handlers);
2867 }
2868 self.at += usize::from(negated);
2869 let h = &mut handlers[head(self)?];
2870 let body = self.block(&stops)?;
2871 if negated { h.not_on = Some(body) } else { h.on = Some(body) }
2872 }
2873 }
2874
2875 fn invalid_key(&mut self, end: &str) -> R<Handlers> {
2877 let [h] = self.on_phrases(&["INVALID"], &[end], |p| {
2878 p.at += 1;
2879 p.accept_word("KEY");
2880 Ok(0)
2881 })?;
2882 self.accept_word(end);
2883 Ok(h)
2884 }
2885
2886 fn json_operand_follows(&self) -> bool {
2887 self.starts_ref() && !self.word().is_some_and(|w| JSON_PHRASES.contains(&w))
2888 }
2889
2890 fn figurative_list(&mut self) -> R<Vec<Figurative>> {
2891 let mut out = Vec::new();
2892 loop {
2893 let word = self.name("a figurative constant")?;
2894 out.push(figurative(&word).filter(|f| !matches!(f, Figurative::Quote | Figurative::Null)).ok_or_else(|| self.error("ZERO, SPACE, LOW-VALUE or HIGH-VALUE"))?);
2895 if !self.accept_word("OR") {
2896 return Ok(out);
2897 }
2898 }
2899 }
2900
2901 fn marker(&mut self, inside: bool) -> R<(Marker, Option<Ref>)> {
2903 if self.starts_ref() {
2904 return Ok((Marker::Condition(self.reference()?), None));
2905 }
2906 let literal = self.literal()?;
2907 let item = if inside {
2908 self.expect_word("IN")?;
2909 Some(self.reference()?)
2910 } else {
2911 None
2912 };
2913 Ok((Marker::Literal(literal), item))
2914 }
2915
2916 fn json_generate(&mut self, pos: Pos) -> R<JsonGenerate> {
2917 let receiver = self.reference()?;
2918 self.expect_word("FROM")?;
2919 let from = self.reference()?;
2920 let mut g = JsonGenerate { receiver, from, count: None, names: Vec::new(), suppress: Vec::new(), converting: Vec::new(), indicating: Vec::new(), encoding: None, on_exception: None, not_on_exception: None, pos };
2921 loop {
2922 if self.accept_word("COUNT") {
2923 self.accept_word("IN");
2924 g.count = Some(self.reference()?);
2925 } else if self.accept_word("INDICATING") {
2926 loop {
2927 let item = self.reference()?;
2928 self.accept_word("IS");
2929 self.accept_word("JSON");
2930 self.expect_word("NULL")?;
2931 self.expect_word("USING")?;
2932 let (marker, indicator) = self.marker(true)?;
2933 g.indicating.push(NullIndicator { item, marker, indicator });
2934 if !self.accept_word("ALSO") {
2935 break;
2936 }
2937 }
2938 } else if self.accept_word("ENCODING") {
2939 g.encoding = Some(if self.accept_word("FROM") {
2940 self.expect_word("CODEPAGE")?;
2941 Encoding::FromCodepage
2942 } else {
2943 Encoding::Ccsid(self.operand()?)
2944 });
2945 } else if self.accept_word("NAME") {
2946 self.accept_word("OF");
2947 loop {
2948 let item = self.reference()?;
2949 self.accept_word("IS");
2950 let name = if self.accept_word("OMITTED") { None } else { Some(self.literal()?) };
2951 g.names.push((item, name));
2952 if !self.json_operand_follows() {
2953 break;
2954 }
2955 }
2956 } else if self.accept_word("SUPPRESS") {
2957 loop {
2958 if self.accept_word("EVERY") {
2959 let numeric = match self.accept_any(&["NUMERIC", "NONNUMERIC"]).as_deref() {
2960 Some("NUMERIC") => Some(true),
2961 Some(_) => Some(false),
2962 None => None,
2963 };
2964 self.expect_word("WHEN")?;
2965 g.suppress.push(Suppression::Every { numeric, form: None, when: self.figurative_list()? });
2966 } else {
2967 let item = self.reference()?;
2968 let when = if self.accept_word("WHEN") { self.figurative_list()? } else { Vec::new() };
2969 g.suppress.push(Suppression::Item { item, when });
2970 }
2971 if !(self.is_word("EVERY") || self.json_operand_follows()) {
2972 break;
2973 }
2974 }
2975 } else if self.accept_word("CONVERTING") {
2976 loop {
2977 let item = self.reference()?;
2978 self.expect_word("TO")?;
2979 self.accept_word("JSON");
2980 let conversion = if self.accept_word("NULL") {
2981 self.expect_word("USING")?;
2982 let [f] = self.figurative_list()?[..] else { return Err(self.error("one figurative constant after USING")) };
2983 JsonConversion::Null(f)
2984 } else {
2985 if self.accept_any(&["BOOLEAN", "BOOL"]).is_none() {
2986 return Err(self.error("BOOLEAN or NULL"));
2987 }
2988 self.expect_word("USING")?;
2989 JsonConversion::Boolean(self.marker(false)?.0)
2990 };
2991 g.converting.push((item, conversion));
2992 if !self.accept_word("ALSO") {
2993 break;
2994 }
2995 }
2996 } else {
2997 break;
2998 }
2999 }
3000 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-JSON"], |p| {
3001 p.accept_word("ON");
3002 p.expect_word("EXCEPTION")?;
3003 Ok(0)
3004 })?;
3005 self.accept_word("END-JSON");
3006 (g.on_exception, g.not_on_exception) = (h.on, h.not_on);
3007 Ok(g)
3008 }
3009
3010 fn flag(&mut self, inside: bool) -> R<(Flag, Option<Ref>)> {
3013 self.expect_word("USING")?;
3014 if self.starts_ref() {
3015 let first = self.reference()?;
3016 if self.accept_word("AND") {
3017 return Ok((Flag::Conditions(first, self.reference()?), None));
3018 }
3019 return Ok((Flag::Condition(first), None));
3020 }
3021 let on = self.literal()?;
3022 self.expect_word("AND")?;
3023 let off = self.literal()?;
3024 let item = if inside {
3025 self.expect_word("IN")?;
3026 Some(self.reference()?)
3027 } else {
3028 None
3029 };
3030 Ok((Flag::Literals(on, off), item))
3031 }
3032
3033 fn json_parse(&mut self, pos: Pos) -> R<JsonParse> {
3034 let source = self.reference()?;
3035 self.expect_word("INTO")?;
3036 let into = self.reference()?;
3037 let mut j = JsonParse {
3038 source,
3039 into,
3040 detail: false,
3041 ignoring: Vec::new(),
3042 indicating: Vec::new(),
3043 encoding: None,
3044 names: Vec::new(),
3045 suppress: Vec::new(),
3046 converting: Vec::new(),
3047 on_exception: None,
3048 not_on_exception: None,
3049 pos,
3050 };
3051 loop {
3052 if self.accept_word("WITH") || self.is_word("DETAIL") {
3053 self.expect_word("DETAIL")?;
3054 j.detail = true;
3055 } else if self.accept_word("IGNORING") {
3056 loop {
3057 self.accept_word("JSON");
3058 self.expect_word("NULL")?;
3059 self.expect_word("FOR")?;
3060 j.ignoring.push(if self.accept_word("ALL") { None } else { Some(self.reference()?) });
3061 if !self.accept_word("ALSO") {
3062 break;
3063 }
3064 }
3065 } else if self.accept_word("INDICATING") {
3066 loop {
3067 let item = self.reference()?;
3068 self.accept_word("IS");
3069 self.accept_word("JSON");
3070 self.expect_word("NULL")?;
3071 let (flag, indicator) = self.flag(true)?;
3072 j.indicating.push((item, flag, indicator));
3073 if !self.accept_word("ALSO") {
3074 break;
3075 }
3076 }
3077 } else if self.accept_word("ENCODING") {
3078 j.encoding = Some(if self.accept_word("FROM") {
3079 self.expect_word("CODEPAGE")?;
3080 Encoding::FromCodepage
3081 } else {
3082 Encoding::Ccsid(self.operand()?)
3083 });
3084 } else if self.accept_word("NAME") {
3085 self.accept_word("OF");
3086 loop {
3087 let item = self.reference()?;
3088 self.accept_word("IS");
3089 let name = if self.accept_word("OMITTED") { None } else { Some(self.literal()?) };
3090 j.names.push((item, name));
3091 if !self.json_operand_follows() {
3092 break;
3093 }
3094 }
3095 } else if self.accept_word("SUPPRESS") {
3096 loop {
3097 j.suppress.push(self.reference()?);
3098 if !self.json_operand_follows() {
3099 break;
3100 }
3101 }
3102 } else if self.accept_word("CONVERTING") {
3103 loop {
3104 let item = self.reference()?;
3105 self.expect_word("FROM")?;
3106 self.accept_word("JSON");
3107 let conversion = if self.accept_word("NULL") {
3108 self.expect_word("USING")?;
3109 let [f] = self.figurative_list()?[..] else { return Err(self.error("one figurative constant after USING")) };
3110 ParseConversion::Null(f)
3111 } else {
3112 if self.accept_any(&["BOOLEAN", "BOOL"]).is_none() {
3113 return Err(self.error("BOOLEAN or NULL"));
3114 }
3115 ParseConversion::Boolean(Box::new(self.flag(false)?.0))
3116 };
3117 j.converting.push((item, conversion));
3118 if !self.accept_word("ALSO") {
3119 break;
3120 }
3121 }
3122 } else {
3123 break;
3124 }
3125 }
3126 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-JSON"], |p| {
3127 p.accept_word("ON");
3128 p.expect_word("EXCEPTION")?;
3129 Ok(0)
3130 })?;
3131 self.accept_word("END-JSON");
3132 (j.on_exception, j.not_on_exception) = (h.on, h.not_on);
3133 Ok(j)
3134 }
3135
3136 fn xml_operand_follows(&self) -> bool {
3137 self.starts_ref() && !self.word().is_some_and(|w| XML_PHRASES.contains(&w))
3138 }
3139
3140 fn xml_form(&mut self) -> R<Option<XmlForm>> {
3141 Ok(match self.accept_any(&["ATTRIBUTE", "ELEMENT", "CONTENT"]).as_deref() {
3142 Some("ATTRIBUTE") => Some(XmlForm::Attribute),
3143 Some("ELEMENT") => Some(XmlForm::Element),
3144 Some(_) => Some(XmlForm::Content),
3145 None => None,
3146 })
3147 }
3148
3149 fn xml_generate(&mut self, pos: Pos) -> R<XmlGenerate> {
3150 let receiver = self.reference()?;
3151 self.expect_word("FROM")?;
3152 let from = self.reference()?;
3153 let mut x = XmlGenerate {
3154 receiver,
3155 from,
3156 count: None,
3157 encoding: None,
3158 declaration: false,
3159 attributes: false,
3160 namespace: None,
3161 prefix: None,
3162 names: Vec::new(),
3163 types: Vec::new(),
3164 suppress: Vec::new(),
3165 on_exception: None,
3166 not_on_exception: None,
3167 pos,
3168 };
3169 loop {
3170 if self.accept_word("COUNT") {
3171 self.accept_word("IN");
3172 x.count = Some(self.reference()?);
3173 continue;
3174 }
3175 let with = self.accept_word("WITH");
3176 if self.accept_word("ENCODING") {
3177 x.encoding = Some(self.operand()?);
3178 } else if self.accept_word("XML-DECLARATION") {
3179 x.declaration = true;
3180 } else if self.accept_word("ATTRIBUTES") {
3181 x.attributes = true;
3182 } else if with {
3183 return Err(self.error("ENCODING, XML-DECLARATION or ATTRIBUTES"));
3184 } else if self.accept_word("NAMESPACE") {
3185 self.accept_word("IS");
3186 x.namespace = Some(self.operand()?);
3187 if self.accept_word("NAMESPACE-PREFIX") {
3188 self.accept_word("IS");
3189 x.prefix = Some(self.operand()?);
3190 }
3191 } else if self.accept_word("NAME") {
3192 self.accept_word("OF");
3193 loop {
3194 let item = self.reference()?;
3195 self.accept_word("IS");
3196 x.names.push((item, self.literal()?));
3197 if !self.xml_operand_follows() {
3198 break;
3199 }
3200 }
3201 } else if self.accept_word("TYPE") {
3202 self.accept_word("OF");
3203 loop {
3204 let item = self.reference()?;
3205 self.accept_word("IS");
3206 let form = self.xml_form()?.ok_or_else(|| self.error("ATTRIBUTE, ELEMENT or CONTENT"))?;
3207 x.types.push((item, form));
3208 if !self.xml_operand_follows() {
3209 break;
3210 }
3211 }
3212 } else if self.accept_word("SUPPRESS") {
3213 loop {
3214 if self.accept_word("EVERY") {
3215 let numeric = match self.accept_any(&["NUMERIC", "NONNUMERIC"]).as_deref() {
3216 Some("NUMERIC") => Some(true),
3217 Some(_) => Some(false),
3218 None => None,
3219 };
3220 let form = self.xml_form()?;
3221 if numeric.is_none() && form.is_none() {
3222 return Err(self.error("NUMERIC, NONNUMERIC, ATTRIBUTE, CONTENT or ELEMENT"));
3223 }
3224 self.expect_word("WHEN")?;
3225 x.suppress.push(Suppression::Every { numeric, form, when: self.figurative_list()? });
3226 } else {
3227 let item = self.reference()?;
3228 let when = if self.accept_word("WHEN") { self.figurative_list()? } else { Vec::new() };
3229 x.suppress.push(Suppression::Item { item, when });
3230 }
3231 if !(self.is_word("EVERY") || self.xml_operand_follows()) {
3232 break;
3233 }
3234 }
3235 } else {
3236 break;
3237 }
3238 }
3239 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-XML"], |p| {
3240 p.accept_word("ON");
3241 p.expect_word("EXCEPTION")?;
3242 Ok(0)
3243 })?;
3244 self.accept_word("END-XML");
3245 (x.on_exception, x.not_on_exception) = (h.on, h.not_on);
3246 Ok(x)
3247 }
3248
3249 fn xml_parse(&mut self, pos: Pos) -> R<XmlParse> {
3250 let document = self.reference()?;
3251 let mut x = XmlParse { document, encoding: None, returning_national: false, procedure: ProcName { name: String::new(), section: None }, thru: None, on_exception: None, not_on_exception: None, pos };
3252 loop {
3253 if self.accept_word("WITH") || self.is_word("ENCODING") {
3254 self.expect_word("ENCODING")?;
3255 x.encoding = Some(self.operand()?);
3256 } else if self.accept_word("RETURNING") {
3257 self.expect_word("NATIONAL")?;
3258 x.returning_national = true;
3259 } else if self.is_word("VALIDATING") {
3260 let at = self.pos();
3262 self.at += 1;
3263 self.accept_word("WITH");
3264 let schema = if self.accept_word("FILE") { format!("FILE {}", self.name("an XML schema name")?) } else { self.reference()?.name };
3265 return Err(crate::messages::IWR0001.at(at, format!("XML PARSE VALIDATING WITH {schema}: the schema is in IBM's Optimized Schema Representation (OSR), which ironwork does not read")));
3266 } else {
3267 break;
3268 }
3269 }
3270 self.expect_word("PROCESSING")?;
3271 self.expect_word("PROCEDURE")?;
3272 self.accept_word("IS");
3273 x.procedure = self.proc_name()?;
3274 if self.accept_any(&["THRU", "THROUGH"]).is_some() {
3275 x.thru = Some(self.proc_name()?);
3276 }
3277 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-XML"], |p| {
3278 p.accept_word("ON");
3279 p.expect_word("EXCEPTION")?;
3280 Ok(0)
3281 })?;
3282 self.accept_word("END-XML");
3283 (x.on_exception, x.not_on_exception) = (h.on, h.not_on);
3284 Ok(x)
3285 }
3286
3287 fn call(&mut self, pos: Pos) -> R<Call> {
3288 let target = self.operand()?;
3289 let mut using = Vec::new();
3290 if self.accept_word("USING") {
3291 let mut mode = ArgMode::Reference;
3292 loop {
3293 let by = self.accept_word("BY");
3294 if let Some(m) = self.accept_any(&["REFERENCE", "CONTENT", "VALUE"]) {
3295 mode = match m.as_str() {
3296 "CONTENT" => ArgMode::Content,
3297 "VALUE" => ArgMode::Value,
3298 _ => ArgMode::Reference,
3299 };
3300 } else if by {
3301 return Err(self.error("REFERENCE, CONTENT or VALUE after BY"));
3302 } else if self.accept_word("OMITTED") {
3303 using.push(Arg { mode, value: None });
3304 } else if self.starts_operand() {
3305 using.push(Arg { mode, value: Some(self.operand()?) });
3306 } else {
3307 break;
3308 }
3309 }
3310 }
3311 let returning = if self.accept_word("RETURNING") { Some(self.reference()?) } else { None };
3312 let [exception] = self.on_phrases(&["ON", "EXCEPTION", "OVERFLOW"], &["END-CALL"], |p| {
3313 p.accept_word("ON");
3314 p.accept_any(&["EXCEPTION", "OVERFLOW"]).map(|_| 0).ok_or_else(|| p.error("EXCEPTION or OVERFLOW"))
3315 })?;
3316 self.accept_word("END-CALL");
3317 Ok(Call { target, using, returning, on_exception: exception.on, not_on_exception: exception.not_on, pos })
3318 }
3319
3320 fn overflow(&mut self, end: &str) -> R<Handlers> {
3322 let [h] = self.on_phrases(&["ON", "OVERFLOW"], &[end], |p| {
3323 p.accept_word("ON");
3324 p.expect_word("OVERFLOW").map(|()| 0)
3325 })?;
3326 self.accept_word(end);
3327 Ok(h)
3328 }
3329
3330 fn string(&mut self, pos: Pos) -> R<StringStmt> {
3331 let mut sources = Vec::new();
3332 while !self.is_word("INTO") {
3333 let mut group = Vec::new();
3334 while self.starts_operand() && !self.is_word("DELIMITED") {
3335 group.push(self.operand()?);
3336 }
3337 if group.is_empty() {
3338 return Err(self.error("a sending item"));
3339 }
3340 let delimiter = if self.accept_word("DELIMITED") {
3341 self.accept_word("BY");
3342 if self.accept_word("SIZE") { Delimiter::Size } else { Delimiter::By(self.operand()?) }
3343 } else {
3344 Delimiter::Size
3345 };
3346 sources.extend(group.into_iter().map(|op| (op, delimiter.clone())));
3347 }
3348 self.expect_word("INTO")?;
3349 let into = self.reference()?;
3350 let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
3351 self.expect_word("POINTER")?;
3352 Some(self.reference()?)
3353 } else {
3354 None
3355 };
3356 let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-STRING")?;
3357 Ok(StringStmt { sources, into, pointer, on_overflow, not_on_overflow, pos })
3358 }
3359
3360 fn unstring(&mut self, pos: Pos) -> R<Unstring> {
3361 let source = self.reference()?;
3362 let mut delimiters = Vec::new();
3363 if self.accept_word("DELIMITED") {
3364 self.accept_word("BY");
3365 loop {
3366 let all = self.accept_word("ALL");
3367 delimiters.push((all, self.operand()?));
3368 if !self.accept_word("OR") {
3369 break;
3370 }
3371 }
3372 }
3373 self.expect_word("INTO")?;
3374 let mut into = Vec::new();
3375 while self.starts_ref() {
3376 let target = self.reference()?;
3377 let delimiter_in = if self.accept_word("DELIMITER") {
3378 self.accept_word("IN");
3379 Some(self.reference()?)
3380 } else {
3381 None
3382 };
3383 let count_in = if self.accept_word("COUNT") {
3384 self.accept_word("IN");
3385 Some(self.reference()?)
3386 } else {
3387 None
3388 };
3389 into.push(UnstringInto { target, delimiter_in, count_in });
3390 }
3391 if into.is_empty() {
3392 return Err(self.error("a receiving item after INTO"));
3393 }
3394 let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
3395 self.expect_word("POINTER")?;
3396 Some(self.reference()?)
3397 } else {
3398 None
3399 };
3400 let tallying = if self.accept_word("TALLYING") {
3401 self.accept_word("IN");
3402 Some(self.reference()?)
3403 } else {
3404 None
3405 };
3406 let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-UNSTRING")?;
3407 Ok(Unstring { source, delimiters, into, pointer, tallying, on_overflow, not_on_overflow, pos })
3408 }
3409
3410 fn bounds(&mut self) -> R<Vec<Bound>> {
3411 let mut bounds = Vec::new();
3412 while let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
3413 self.accept_word("INITIAL");
3414 bounds.push(Bound { after: side == "AFTER", value: self.operand()? });
3415 }
3416 Ok(bounds)
3417 }
3418
3419 fn allocate(&mut self, pos: Pos) -> R<Stmt> {
3428 let ends = |w: &str| matches!(w, "INITIALIZED" | "LOC" | "RETURNING");
3429 let characters = self.tokens[self.at..]
3430 .iter()
3431 .take_while(|t| t.tok != Tok::Period && !matches!(&t.tok, Tok::Word(w) if ends(w) || self.is_verb(w) && w != "FUNCTION"))
3432 .any(|t| matches!(&t.tok, Tok::Word(w) if w == "CHARACTERS"));
3433 let (item, size) = if characters {
3434 let size = self.expr()?;
3435 self.expect_word("CHARACTERS")?;
3436 (None, size)
3437 } else {
3438 let r = self.reference()?;
3439 (Some(r.clone()), Expr::Operand(Operand::LengthOf(r)))
3440 };
3441 let initialized = self.accept_word("INITIALIZED");
3442 if self.accept_word("LOC") {
3443 match self.peek() {
3444 Some(Tok::Number(n)) if matches!(n.as_str(), "24" | "31" | "64") => self.at += 1,
3445 _ => return Err(self.error("24, 31 or 64 after LOC")),
3446 }
3447 }
3448 let returning = if self.accept_word("RETURNING") { Some(self.reference()?) } else { None };
3449 let heap = Operand::Function(FunctionCall { name: "HEAP ALLOCATE".into(), args: vec![size], modifier: None, refmod: None, all_subscripts: Vec::new(), pos });
3450 let Some(item) = item else {
3451 let Some(pointer) = returning else { return Err(self.error("RETURNING after ALLOCATE ... CHARACTERS")) };
3452 return Ok(Stmt::Set { set: SetStmt::To { targets: vec![pointer], value: heap }, pos });
3453 };
3454 let mut statements = vec![Stmt::Set { set: SetStmt::AddressOf { targets: vec![item.clone()], value: heap }, pos }];
3455 if initialized {
3456 let with = InitializeWith { filler: true, value: DataCategory::ALL.to_vec(), replacing: Vec::new(), default: true };
3457 statements.push(Stmt::Initialize { targets: vec![item.clone()], with: Some(Box::new(with)), pos });
3458 }
3459 if let Some(pointer) = returning {
3460 statements.push(Stmt::Set { set: SetStmt::To { targets: vec![pointer], value: Operand::AddressOf(item) }, pos });
3461 }
3462 let last = statements.pop().unwrap_or(Stmt::Continue { pos });
3463 self.before.extend(statements);
3464 Ok(last)
3465 }
3466
3467 fn free(&mut self, pos: Pos) -> R<Stmt> {
3471 let mut statements = Vec::new();
3472 while self.starts_ref() && !self.word().is_some_and(|w| self.is_verb(w) || w.starts_with("END-") || PHRASE_WORDS.contains(&w)) {
3473 let at = self.pos();
3474 let r = self.reference()?;
3475 let freed = |address: Operand| Operand::Function(FunctionCall { name: "HEAP FREE".into(), args: vec![Expr::Operand(address)], modifier: None, refmod: None, all_subscripts: Vec::new(), pos });
3476 if r.qualifiers.is_empty() && self.unstored.contains(&r.name) {
3477 if !self.extended {
3478 return Err(crate::messages::IWC0317.at(at, format!("FREE {}: GnuCOBOL's FREE of a record, not Enterprise COBOL's, which frees through a pointer; --compliance extended reads it", r.name)));
3479 }
3480 self.messages.push(crate::messages::IWX0047.at(at, format!("FREE {} (GnuCOBOL; Enterprise COBOL frees through a pointer): the storage ADDRESS OF {0} names is released, and the record has none", r.name)));
3481 statements.push(Stmt::Set { set: SetStmt::AddressOf { targets: vec![r.clone()], value: freed(Operand::AddressOf(r)) }, pos });
3482 } else {
3483 statements.push(Stmt::Set { set: SetStmt::To { targets: vec![r.clone()], value: freed(Operand::Ref(r)) }, pos });
3484 }
3485 }
3486 let last = statements.pop().ok_or_else(|| self.error("a pointer after FREE"))?;
3487 self.before.extend(statements);
3488 Ok(last)
3489 }
3490
3491 fn initialize(&mut self, pos: Pos) -> R<Stmt> {
3492 let category = |p: &Self| p.word().and_then(DataCategory::from_word);
3493 let mut targets = Vec::new();
3494 while self.starts_ref() && category(self).is_none() && !self.is_word("FILLER") {
3495 targets.push(self.reference()?);
3496 }
3497 if targets.is_empty() {
3498 return Err(self.error("a data name"));
3499 }
3500 let twice = |c: DataCategory, phrase: &str| crate::messages::IWS0059.at(pos, format!("INITIALIZE: {} is named twice in the {phrase} phrase", c.word()));
3501 let mut with = InitializeWith::default();
3502 if self.is_word("FILLER") || self.is_word("WITH") && self.word_at(1) == Some("FILLER") {
3503 self.accept_word("WITH");
3504 self.at += 1;
3505 with.filler = true;
3506 }
3507 loop {
3508 let named = if self.accept_word("ALL") {
3509 DataCategory::ALL.to_vec()
3510 } else if let Some(c) = category(self) {
3511 self.at += 1;
3512 vec![c]
3513 } else {
3514 break;
3515 };
3516 self.expect_word("TO")?;
3517 self.expect_word("VALUE")?;
3518 for c in named {
3519 if with.value.contains(&c) {
3520 return Err(twice(c, "VALUE"));
3521 }
3522 with.value.push(c);
3523 }
3524 }
3525 if self.is_word("THEN") && self.word_at(1) == Some("REPLACING") {
3526 self.at += 1;
3527 }
3528 if self.accept_word("REPLACING") {
3529 while let Some(c) = category(self) {
3530 self.at += 1;
3531 self.accept_word("DATA");
3532 self.expect_word("BY")?;
3533 let by = self.operand()?;
3534 if with.replacing.iter().any(|(d, _)| *d == c) {
3535 return Err(twice(c, "REPLACING"));
3536 }
3537 with.replacing.push((c, by));
3538 }
3539 if with.replacing.is_empty() {
3540 return Err(self.error("a category after REPLACING"));
3541 }
3542 }
3543 let then = usize::from(self.is_word("THEN"));
3544 if self.word_at(then) == Some("TO") && self.word_at(then + 1) == Some("DEFAULT") {
3545 self.at += then + 2;
3546 with.default = true;
3547 }
3548 let with = (with != InitializeWith::default()).then(|| Box::new(with));
3549 Ok(Stmt::Initialize { targets, with, pos })
3550 }
3551
3552 fn trailing(&mut self) -> Option<String> {
3554 if !self.extended || !self.is_word("TRAILING") {
3555 return None;
3556 }
3557 let at = self.pos();
3558 self.at += 1;
3559 self.messages.push(crate::messages::IWX0023.at(at, "INSPECT ... TRAILING (GnuCOBOL; Enterprise COBOL has ALL, LEADING, FIRST and CHARACTERS): the occurrences that run on to the end of the phrase's region"));
3560 Some("TRAILING".into())
3561 }
3562
3563 fn inspect(&mut self, pos: Pos) -> R<Inspect> {
3564 let target = if self.is_word("FUNCTION") { self.operand()? } else { Operand::Ref(self.reference()?) };
3565 let (mut tallying, mut replacing, mut converting) = (Vec::new(), Vec::new(), None);
3566 if self.accept_word("TALLYING") {
3567 while self.starts_ref() && self.word_at(1) == Some("FOR") || self.starts_ref() && !self.is_word("REPLACING") && self.tally_counter_ahead() {
3568 let counter = self.reference()?;
3569 self.expect_word("FOR")?;
3570 loop {
3571 if self.accept_word("CHARACTERS") {
3572 tallying.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
3573 } else if let Some(mode) = self.accept_any(&["ALL", "LEADING"]).or_else(|| self.trailing()) {
3574 let mode = match mode.as_str() {
3575 "ALL" => InspectMode::All,
3576 "LEADING" => InspectMode::Leading,
3577 _ => InspectMode::Trailing,
3578 };
3579 loop {
3580 let pattern = self.operand()?;
3581 tallying.push(InspectPhrase { mode, pattern: Some(pattern), by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
3582 if !self.starts_operand() || self.word_at(1) == Some("FOR") || self.is_word("ALL") || self.is_word("LEADING") || self.is_word("TRAILING") {
3583 break;
3584 }
3585 }
3586 } else {
3587 break;
3588 }
3589 }
3590 }
3591 }
3592 if self.accept_word("REPLACING") {
3593 loop {
3594 if self.accept_word("CHARACTERS") {
3595 self.expect_word("BY")?;
3596 let by = self.operand()?;
3597 replacing.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: Some(by), counter: None, bounds: self.bounds()? });
3598 } else if let Some(mode) = self.accept_any(&["ALL", "LEADING", "FIRST"]).or_else(|| self.trailing()) {
3599 let mode = match mode.as_str() {
3600 "ALL" => InspectMode::All,
3601 "LEADING" => InspectMode::Leading,
3602 "FIRST" => InspectMode::First,
3603 _ => InspectMode::Trailing,
3604 };
3605 loop {
3606 let pattern = self.operand()?;
3607 self.expect_word("BY")?;
3608 let by = self.operand()?;
3609 replacing.push(InspectPhrase { mode, pattern: Some(pattern), by: Some(by), counter: None, bounds: self.bounds()? });
3610 if !self.starts_operand() || self.is_word("ALL") || self.is_word("LEADING") || self.is_word("FIRST") || self.is_word("TRAILING") {
3611 break;
3612 }
3613 }
3614 } else {
3615 break;
3616 }
3617 }
3618 }
3619 if self.accept_word("CONVERTING") {
3620 let from = self.operand()?;
3621 self.expect_word("TO")?;
3622 let to = self.operand()?;
3623 converting = Some((from, to, self.bounds()?));
3624 }
3625 if tallying.is_empty() && replacing.is_empty() && converting.is_none() {
3626 return Err(self.error("TALLYING, REPLACING or CONVERTING"));
3627 }
3628 Ok(Inspect { target, tallying, replacing, converting, pos })
3629 }
3630
3631 fn tally_counter_ahead(&self) -> bool {
3633 let mut i = self.at + 1;
3634 if self.qualifying_paren_at(i) {
3635 let mut depth = 0;
3636 while let Some(t) = self.tokens.get(i) {
3637 match t.tok {
3638 Tok::LParen => depth += 1,
3639 Tok::RParen => {
3640 depth -= 1;
3641 if depth == 0 {
3642 i += 1;
3643 break;
3644 }
3645 }
3646 _ => {}
3647 }
3648 i += 1;
3649 }
3650 }
3651 matches!(self.tokens.get(i).map(|t| &t.tok), Some(Tok::Word(w)) if w == "FOR")
3652 }
3653
3654 fn search(&mut self, pos: Pos) -> R<Search> {
3655 let all = self.accept_word("ALL");
3656 let table = self.reference()?;
3657 let varying = if self.accept_word("VARYING") { Some(self.reference()?) } else { None };
3658 let at_end = if self.accept_word("AT") || self.is_word("END") {
3659 self.expect_word("END")?;
3660 Some(self.block(&["WHEN"])?)
3661 } else {
3662 None
3663 };
3664 let mut whens = Vec::new();
3665 while self.accept_word("WHEN") {
3666 let cond = self.cond()?;
3667 let body = self.block(&["WHEN", "END-SEARCH"])?;
3668 whens.push((cond, body));
3669 }
3670 if whens.is_empty() {
3671 return Err(self.error("WHEN"));
3672 }
3673 self.accept_word("END-SEARCH");
3674 Ok(Search { table, all, varying, at_end, whens, pos })
3675 }
3676
3677 fn screen_size(&mut self) -> Option<&'static str> {
3680 let size = match (self.extended && self.is_word("FROM"), self.word_at(1)) {
3681 (true, Some("LINES")) => "24",
3682 (true, Some("COLUMNS" | "COLS")) => "80",
3683 _ => return None,
3684 };
3685 let at = self.tokens[self.at + 1].pos;
3686 let word = self.word_at(1).unwrap_or_default().to_owned();
3687 let unit = if size == "24" { "lines" } else { "columns" };
3688 self.at += 2;
3689 self.messages.push(crate::messages::IWX0029.at(at, format!("ACCEPT ... FROM {word} (GnuCOBOL; Enterprise COBOL has no screen): the screen's {size} {unit}")));
3690 Some(size)
3691 }
3692
3693 fn accept_device(&self, name: &str, pos: Pos) -> R<()> {
3698 let device = self.mnemonics.iter().find(|(m, _)| m == name).map_or(name, |(_, e)| e.as_str());
3699 if ACCEPT_DEVICES.contains(&device) {
3700 return Ok(());
3701 }
3702 let why = if GNUCOBOL_ACCEPT_SOURCES.contains(&name) {
3703 "GnuCOBOL's, not Enterprise COBOL's".to_owned()
3704 } else if device == name {
3705 "neither an environment-name ACCEPT reads, SYSIN, SYSIPT or CONSOLE, nor a mnemonic-name for one".to_owned()
3706 } else {
3707 format!("a mnemonic-name for {device}, which ACCEPT does not read")
3708 };
3709 Err(crate::messages::IWS0060.at(pos, format!("ACCEPT ... FROM {name}: {why}")))
3710 }
3711
3712 fn set(&mut self) -> R<SetStmt> {
3713 if self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
3714 let mut targets = Vec::new();
3715 while self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
3716 self.at += 2;
3717 targets.push(self.reference()?);
3718 }
3719 self.expect_word("TO")?;
3720 return Ok(SetStmt::AddressOf { targets, value: self.operand()? });
3721 }
3722 let targets = self.refs()?;
3723 if self.accept_word("TO") {
3724 if self.accept_word("TRUE") {
3725 return Ok(SetStmt::ConditionTrue(targets));
3726 }
3727 if self.accept_word("FALSE") {
3728 return Ok(SetStmt::ConditionFalse(targets));
3729 }
3730 if self.accept_word("ENTRY") {
3731 return Ok(SetStmt::Entry { targets, entry: self.operand()? });
3732 }
3733 if let Some(status) = self.accept_any(&["ON", "OFF"]) {
3734 let mut groups = vec![(targets, status == "ON")];
3735 while self.starts_ref() {
3736 let targets = self.refs()?;
3737 self.expect_word("TO")?;
3738 let status = self.accept_any(&["ON", "OFF"]).ok_or_else(|| self.error("ON or OFF"))?;
3739 groups.push((targets, status == "ON"));
3740 }
3741 return Ok(SetStmt::Switches(groups));
3742 }
3743 return Ok(SetStmt::To { targets, value: self.operand()? });
3744 }
3745 match self.accept_any(&["UP", "DOWN"]).as_deref() {
3746 Some(direction) => {
3747 self.expect_word("BY")?;
3748 Ok(SetStmt::UpDown { targets, down: direction == "DOWN", by: self.expr()? })
3749 }
3750 None => Err(self.error("TO, UP BY or DOWN BY")),
3751 }
3752 }
3753
3754 fn proc_name(&mut self) -> R<ProcName> {
3755 let name = self.procedure_word("a procedure name")?;
3756 let section = if self.accept_any(&["OF", "IN"]).is_some() { Some(self.procedure_word("a section name")?) } else { None };
3757 Ok(ProcName { name, section })
3758 }
3759
3760 fn procedure_word(&mut self, what: &str) -> R<String> {
3762 match self.peek() {
3763 Some(t) if digits(t) => {
3764 let Tok::Number(n) = t.clone() else { unreachable!() };
3765 self.at += 1;
3766 Ok(n)
3767 }
3768 _ => self.name(what),
3769 }
3770 }
3771
3772 fn starts_proc_name(&self) -> bool {
3773 self.starts_ref() || self.peek().is_some_and(digits) && !self.paragraph_header() && !self.section_header()
3774 }
3775
3776 fn times_ahead(&mut self) -> bool {
3778 let at = self.at;
3779 let times = self.starts_operand() && self.expr().is_ok() && self.is_word("TIMES");
3780 self.at = at;
3781 times
3782 }
3783
3784 fn evaluate(&mut self, pos: Pos) -> R<Stmt> {
3785 let mut subjects = vec![self.subject()?];
3786 while self.accept_word("ALSO") {
3787 subjects.push(self.subject()?);
3788 }
3789 let (mut whens, mut other) = (Vec::new(), Vec::new());
3790 while self.is_word("WHEN") {
3791 if self.word_at(1) == Some("OTHER") {
3792 self.at += 2;
3793 other = self.block(&["END-EVALUATE"])?;
3794 break;
3795 }
3796 let mut alternatives = Vec::new();
3797 while self.is_word("WHEN") && self.word_at(1) != Some("OTHER") {
3798 self.at += 1;
3799 let mut objects = Vec::new();
3800 for (k, subject) in subjects.iter().enumerate() {
3801 if k > 0 {
3802 self.expect_word("ALSO")?;
3803 }
3804 objects.push(self.object(subject)?);
3805 }
3806 alternatives.push(objects);
3807 }
3808 let body = self.block(&["WHEN", "END-EVALUATE"])?;
3809 whens.push(When { alternatives, body });
3810 }
3811 self.accept_word("END-EVALUATE");
3812 Ok(Stmt::Evaluate { subjects, whens, other, pos })
3813 }
3814
3815 fn subject(&mut self) -> R<Subject> {
3816 if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
3817 return Ok(Subject::Bool(b == "TRUE"));
3818 }
3819 let save = self.at;
3820 let conditional = self.expr().is_err()
3822 || self.relop_ahead(0)
3823 || self.is_word("IS")
3824 || self.is_word("NOT")
3825 || self.word().is_some_and(|w| matches!(w, "NUMERIC" | "ALPHABETIC" | "ALPHABETIC-LOWER" | "ALPHABETIC-UPPER" | "DBCS" | "KANJI" | "POSITIVE" | "NEGATIVE" | "ZERO"));
3826 self.at = save;
3827 Ok(if conditional { Subject::Cond(self.cond()?) } else { Subject::Expr(self.expr()?) })
3828 }
3829
3830 fn object(&mut self, subject: &Subject) -> R<Object> {
3831 if self.accept_word("ANY") {
3832 return Ok(Object::Any);
3833 }
3834 if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
3835 return Ok(Object::Bool(b == "TRUE"));
3836 }
3837 if !matches!(subject, Subject::Expr(_)) {
3838 return Ok(Object::Cond(self.cond()?));
3839 }
3840 let not = self.accept_word("NOT");
3841 let from = self.expr()?;
3842 let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.expr()?) } else { None };
3843 Ok(Object::Value { not, from, thru })
3844 }
3845
3846 fn repeat(&mut self) -> R<Loop> {
3847 let mut test_after = false;
3848 if self.accept_word("WITH") || self.is_word("TEST") {
3849 self.expect_word("TEST")?;
3850 test_after = self.accept_any(&["BEFORE", "AFTER"]).as_deref() == Some("AFTER");
3851 }
3852 if self.is_word("UNTIL") && self.word_at(1) == Some("EXIT") {
3853 let at = self.pos();
3854 self.at += 2;
3855 if !self.extended {
3856 return Err(crate::messages::IWC0309.at(at, "PERFORM UNTIL EXIT: GnuCOBOL's and Micro Focus's, not Enterprise COBOL's; --compliance extended reads it"));
3857 }
3858 self.messages.push(crate::messages::IWX0038.at(at, "PERFORM UNTIL EXIT (GnuCOBOL and Micro Focus; Enterprise COBOL has no such condition): it repeats until EXIT PERFORM, GO TO, GOBACK or STOP RUN leaves it"));
3859 return Ok(Loop::Forever);
3860 }
3861 if self.accept_word("UNTIL") {
3862 return Ok(Loop::Until { cond: self.cond()?, test_after });
3863 }
3864 if self.extended && self.is_word("FOREVER") {
3865 let at = self.pos();
3866 self.at += 1;
3867 self.messages.push(crate::messages::IWX0028.at(at, "PERFORM ... FOREVER (Micro Focus and GnuCOBOL; Enterprise COBOL has no FOREVER phrase): it repeats until EXIT PERFORM, GO TO, GOBACK or STOP RUN leaves it"));
3868 return Ok(Loop::Forever);
3869 }
3870 if self.accept_word("VARYING") {
3871 let varying = Box::new(self.varying()?);
3872 let mut after = Vec::new();
3873 while self.is_word("AFTER") {
3874 if after.len() == 6 {
3875 return Err(self.error("the end of the PERFORM: Enterprise COBOL takes at most six AFTER phrases"));
3876 }
3877 self.at += 1;
3878 after.push(self.varying()?);
3879 }
3880 return Ok(Loop::Varying { varying, after, test_after });
3881 }
3882 if self.times_ahead() {
3883 let count = self.expr()?;
3884 self.expect_word("TIMES")?;
3885 return Ok(Loop::Times(count));
3886 }
3887 Ok(Loop::Once)
3888 }
3889
3890 fn varying(&mut self) -> R<Varying> {
3892 let var = self.reference()?;
3893 self.expect_word("FROM")?;
3894 let from = self.expr()?;
3895 self.expect_word("BY")?;
3896 let by = self.expr()?;
3897 self.expect_word("UNTIL")?;
3898 Ok(Varying { var, from, by, until: self.cond()? })
3899 }
3900
3901 fn starts_ref(&self) -> bool {
3902 self.word().is_some_and(|w| !self.is_verb(w) && !PHRASE_WORDS.contains(&w) && figurative(w).is_none() && w != "FUNCTION")
3903 && !self.paragraph_header()
3904 }
3905
3906 fn starts_operand(&self) -> bool {
3907 match self.peek() {
3908 Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Dbcs(_)) => true,
3909 Some(Tok::Number(_)) => !self.paragraph_header(),
3910 Some(Tok::Word(w)) => {
3911 (figurative(w).is_some() || matches!(w.as_str(), "ALL" | "FUNCTION" | "LENGTH" | "ADDRESS" | "DFHRESP" | "DFHVALUE") || self.starts_ref()) && !self.paragraph_header()
3912 }
3913 _ => false,
3914 }
3915 }
3916
3917 fn function_call(&mut self, pos: Pos) -> R<Operand> {
3919 let name = self.name("a function name")?;
3920 let (mut args, mut modifier, mut all_subscripts) = (Vec::new(), None, Vec::new());
3921 if self.qualifying_paren_at(self.at) && !self.refmod_ahead() {
3922 self.at += 1;
3923 while !self.accept(&Tok::RParen) {
3924 if let Some(m) = self.accept_any(&["LEADING", "TRAILING"]) {
3925 modifier = Some(m);
3926 continue;
3927 }
3928 if self.all_subscript_ahead() {
3929 let (table, all) = self.table_with_all()?;
3930 all_subscripts.push((args.len(), all));
3931 args.push(Expr::Operand(Operand::Ref(table)));
3932 continue;
3933 }
3934 args.push(self.expr()?);
3935 }
3936 }
3937 let refmod = self.refmod()?;
3938 Ok(Operand::Function(FunctionCall { name, args, modifier, refmod, all_subscripts, pos }))
3939 }
3940
3941 fn operand(&mut self) -> R<Operand> {
3942 let pos = self.pos();
3943 match self.peek() {
3944 Some(Tok::Word(w)) if w == "FUNCTION" => {
3945 self.at += 1;
3946 self.function_call(pos)
3947 }
3948 Some(Tok::Word(w)) if self.intrinsics.contains(w) && !(w == "LENGTH" && self.word_at(1) == Some("OF")) => self.function_call(pos),
3949 Some(Tok::Word(w)) if w == "LENGTH" && self.word_at(1) == Some("OF") => {
3950 self.at += 2;
3951 Ok(Operand::LengthOf(self.reference()?))
3952 }
3953 Some(Tok::Word(w)) if w == "DFHRESP" && self.peek_at(1) == Some(&Tok::LParen) => {
3954 self.at += 2;
3955 let condition = self.name("a CICS condition")?;
3956 self.expect(&Tok::RParen, "')'")?;
3957 let code = crate::system::resp_code(&condition).ok_or_else(|| crate::messages::IWS0062.at(pos, format!("DFHRESP({condition}): not a CICS condition ironwork for COBOL knows")))?;
3958 Ok(Operand::Literal(Literal::Number(code.to_string())))
3959 }
3960 Some(Tok::Word(w)) if w == "DFHVALUE" && self.peek_at(1) == Some(&Tok::LParen) => {
3961 self.at += 2;
3962 let name = self.name("a CVDA value")?;
3963 self.expect(&Tok::RParen, "')'")?;
3964 let value = rt::cics_tables::cvda(&name).ok_or_else(|| crate::messages::IWS0063.at(pos, format!("DFHVALUE({name}): not a CVDA ironwork for COBOL knows")))?;
3965 Ok(Operand::Literal(Literal::Number(value.to_string())))
3966 }
3967 Some(Tok::Word(w)) if w == "ADDRESS" && self.word_at(1) == Some("OF") => {
3968 self.at += 2;
3969 Ok(Operand::AddressOf(self.reference()?))
3970 }
3971 Some(Tok::Word(w)) if figurative(w).is_some() || w == "ALL" => Ok(Operand::Literal(self.literal()?)),
3972 Some(Tok::Word(_)) => Ok(Operand::Ref(self.reference()?)),
3973 Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Dbcs(_) | Tok::Number(_)) => Ok(Operand::Literal(self.literal()?)),
3974 _ => Err(self.error("an operand")),
3975 }
3976 }
3977
3978 fn refmod_ahead(&self) -> bool {
3980 let mut depth = 0;
3981 for t in &self.tokens[self.at..] {
3982 match t.tok {
3983 Tok::LParen => depth += 1,
3984 Tok::RParen => {
3985 depth -= 1;
3986 if depth == 0 {
3987 return false;
3988 }
3989 }
3990 Tok::Colon if depth == 1 => return true,
3991 Tok::Period => return false,
3992 _ => {}
3993 }
3994 }
3995 false
3996 }
3997
3998 fn refmod(&mut self) -> R<Option<RefMod>> {
3999 if !self.qualifying_paren_at(self.at) || !self.refmod_ahead() {
4000 return Ok(None);
4001 }
4002 self.at += 1;
4003 let start = Box::new(self.expr()?);
4004 self.expect(&Tok::Colon, "':'")?;
4005 let length = if self.peek() == Some(&Tok::RParen) { None } else { Some(Box::new(self.expr()?)) };
4006 self.expect(&Tok::RParen, "')'")?;
4007 Ok(Some(RefMod { start, length }))
4008 }
4009
4010 fn all_subscript_ahead(&self) -> bool {
4012 if !self.starts_ref() {
4013 return false;
4014 }
4015 let mut at = self.at + 1;
4016 while matches!(self.tokens.get(at).map(|t| &t.tok), Some(Tok::Word(w)) if w == "OF" || w == "IN") {
4017 at += 2;
4018 }
4019 if !self.qualifying_paren_at(at) {
4020 return false;
4021 }
4022 let mut depth = 0;
4023 for t in &self.tokens[at..] {
4024 match &t.tok {
4025 Tok::LParen => depth += 1,
4026 Tok::RParen if depth == 1 => return false,
4027 Tok::RParen => depth -= 1,
4028 Tok::Word(w) if depth == 1 && w == "ALL" => return true,
4029 Tok::Period => return false,
4030 _ => {}
4031 }
4032 }
4033 false
4034 }
4035
4036 fn table_with_all(&mut self) -> R<(Ref, Vec<usize>)> {
4039 let pos = self.pos();
4040 let name = self.name("a data name")?;
4041 let mut qualifiers = Vec::new();
4042 while self.accept_any(&["OF", "IN"]).is_some() {
4043 qualifiers.push(self.name("a qualifier")?);
4044 }
4045 self.expect(&Tok::LParen, "'('")?;
4046 let (mut subscripts, mut all) = (Vec::new(), Vec::new());
4047 while !self.accept(&Tok::RParen) {
4048 if self.accept_any(&["ALL"]).is_some() {
4049 all.push(subscripts.len());
4050 subscripts.push(Expr::Operand(Operand::Literal(Literal::Number("1".into()))));
4051 } else {
4052 subscripts.push(self.expr()?);
4053 }
4054 }
4055 let refmod = self.refmod()?;
4056 Ok((Ref { name, qualifiers, subscripts, refmod, pos }, all))
4057 }
4058
4059 fn reference(&mut self) -> R<Ref> {
4060 let pos = self.pos();
4061 let name = self.name("a data name")?;
4062 if name == "WHEN-COMPILED" && !self.registers.contains(&name) {
4063 self.registers.push(name.clone());
4064 }
4065 let mut qualifiers = Vec::new();
4066 while self.accept_any(&["OF", "IN"]).is_some() {
4067 qualifiers.push(self.name("a qualifier")?);
4068 }
4069 let mut subscripts = Vec::new();
4070 if self.qualifying_paren_at(self.at) && !self.refmod_ahead() {
4071 self.at += 1;
4072 while !self.accept(&Tok::RParen) {
4073 subscripts.push(self.expr()?);
4074 }
4075 }
4076 let refmod = self.refmod()?;
4077 Ok(Ref { name, qualifiers, subscripts, refmod, pos })
4078 }
4079
4080 fn expr(&mut self) -> R<Expr> {
4081 let mut left = self.term()?;
4082 loop {
4083 let op = match self.peek() {
4084 Some(Tok::Plus) => BinOp::Add,
4085 Some(Tok::Minus) => BinOp::Sub,
4086 _ => return Ok(left),
4087 };
4088 self.at += 1;
4089 left = Expr::Bin(Box::new(left), op, Box::new(self.term()?));
4090 }
4091 }
4092
4093 fn term(&mut self) -> R<Expr> {
4094 let mut left = self.power()?;
4095 loop {
4096 let op = match self.peek() {
4097 Some(Tok::Star) => BinOp::Mul,
4098 Some(Tok::Slash) => BinOp::Div,
4099 _ => return Ok(left),
4100 };
4101 self.at += 1;
4102 left = Expr::Bin(Box::new(left), op, Box::new(self.power()?));
4103 }
4104 }
4105
4106 fn power(&mut self) -> R<Expr> {
4107 let mut left = self.unary()?;
4108 while self.accept(&Tok::Power) {
4109 left = Expr::Bin(Box::new(left), BinOp::Pow, Box::new(self.unary()?));
4110 }
4111 Ok(left)
4112 }
4113
4114 fn unary(&mut self) -> R<Expr> {
4115 if self.accept(&Tok::Minus) {
4116 return Ok(Expr::Neg(Box::new(self.unary()?)));
4117 }
4118 self.accept(&Tok::Plus);
4119 if self.accept(&Tok::LParen) {
4120 let e = self.expr()?;
4121 self.expect(&Tok::RParen, "')'")?;
4122 return Ok(e);
4123 }
4124 Ok(Expr::Operand(self.operand()?))
4125 }
4126
4127 fn cond(&mut self) -> R<Cond> {
4128 let mut last = None;
4129 self.or_cond(&mut last)
4130 }
4131
4132 fn or_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
4133 let mut left = self.and_cond(last)?;
4134 while self.accept_word("OR") {
4135 left = Cond::Or(Box::new(left), Box::new(self.and_cond(last)?));
4136 }
4137 Ok(left)
4138 }
4139
4140 fn and_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
4141 let mut left = self.not_cond(last)?;
4142 while self.accept_word("AND") {
4143 left = Cond::And(Box::new(left), Box::new(self.not_cond(last)?));
4144 }
4145 Ok(left)
4146 }
4147
4148 fn not_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
4149 if self.is_word("NOT") && !self.relop_ahead(1) {
4150 self.at += 1;
4151 return Ok(Cond::Not(Box::new(self.not_cond(last)?)));
4152 }
4153 self.primary_cond(last)
4154 }
4155
4156 fn relop_ahead(&self, ahead: usize) -> bool {
4157 matches!(self.peek_at(ahead), Some(Tok::Eq | Tok::Lt | Tok::Gt | Tok::Le | Tok::Ge))
4158 || matches!(self.word_at(ahead), Some("EQUAL" | "GREATER" | "LESS"))
4159 }
4160
4161 fn primary_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
4162 let is = usize::from(self.is_word("IS"));
4163 if let Some((subject, ..)) = last.clone()
4164 && (self.relop_ahead(is) || self.word_at(is) == Some("NOT") && self.relop_ahead(is + 1))
4165 {
4166 self.accept_word("IS");
4167 let negated = self.accept_word("NOT");
4168 let op = self.relop()?.ok_or_else(|| self.error("a relational operator"))?;
4169 return self.objects(subject, op, negated, last);
4170 }
4171 if self.peek() == Some(&Tok::LParen) {
4172 let save = self.at;
4173 self.at += 1;
4174 let mut inner_last = last.clone();
4177 if let Ok(c) = self.or_cond(&mut inner_last)
4178 && self.accept(&Tok::RParen)
4179 && !matches!(self.peek(), Some(Tok::Plus | Tok::Minus | Tok::Star | Tok::Slash | Tok::Power))
4180 && !self.relop_ahead(0)
4181 && !self.is_word("IS")
4182 {
4183 return Ok(c);
4184 }
4185 self.at = save;
4186 }
4187 let start = self.at;
4188 let left = self.expr()?;
4189 self.accept_word("IS");
4190 let negated = self.is_word("NOT") && {
4191 self.at += 1;
4192 true
4193 };
4194 let wrap = |c: Cond| if negated { Cond::Not(Box::new(c)) } else { c };
4195 if let Some(op) = self.relop()? {
4196 return self.objects(left, op, negated, last);
4197 }
4198 if let Some(name) = self.word().filter(|w| self.classes.iter().any(|c| c.name == *w)).map(str::to_owned) {
4199 self.at += 1;
4200 return Ok(wrap(Cond::Class(left, Class::Named(name))));
4201 }
4202 if let Some(class) = self.accept_any(&["NUMERIC", "ALPHABETIC", "ALPHABETIC-LOWER", "ALPHABETIC-UPPER", "DBCS", "KANJI", "POSITIVE", "NEGATIVE", "ZERO"]) {
4203 let class = match class.as_str() {
4204 "NUMERIC" => Class::Numeric,
4205 "ALPHABETIC" => Class::Alphabetic,
4206 "ALPHABETIC-LOWER" => Class::AlphabeticLower,
4207 "ALPHABETIC-UPPER" => Class::AlphabeticUpper,
4208 "DBCS" => Class::Dbcs,
4209 "KANJI" => Class::Kanji,
4210 "POSITIVE" => Class::Positive,
4211 "NEGATIVE" => Class::Negative,
4212 _ => Class::Zero,
4213 };
4214 return Ok(wrap(Cond::Class(left, class)));
4215 }
4216 if let Expr::Operand(Operand::Ref(r)) = &left
4217 && self.is_word("OMITTED")
4218 {
4219 return self.omitted(r, negated).map(wrap);
4220 }
4221 if negated {
4222 return Err(self.error("a relational operator or class after NOT"));
4223 }
4224 match (left, last.clone()) {
4225 (Expr::Operand(Operand::Ref(name)), Some((subject, op, negated))) if self.abbreviation_context(start) => Ok(Cond::NameOrRel { subject, op, negated, name }),
4226 (right, Some((subject, op, negated))) if !matches!(&right, Expr::Operand(Operand::Ref(_))) => Ok(relation(subject, op, negated, right)),
4227 (Expr::Operand(Operand::Ref(r)), _) => Ok(Cond::Name(r)),
4228 (_, _) => Err(self.error("a relational operator")),
4229 }
4230 }
4231
4232 fn objects(&mut self, subject: Expr, op: RelOp, negated: bool, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
4235 *last = Some((subject.clone(), op, negated));
4236 if self.peek() == Some(&Tok::LParen) && self.word_at(1) == Some("NOT") && !self.relop_ahead(2) {
4237 return Err(crate::messages::IWS0064.at(self.pos(), "NOT cannot follow the left parenthesis that distributes a relational operator"));
4238 }
4239 let start = self.at;
4240 match self.distributed(&subject, op, negated) {
4241 Ok(Some(c)) => return Ok(c),
4242 _ => self.at = start,
4243 }
4244 let object = self.expr()?;
4245 Ok(relation(subject, op, negated, object))
4246 }
4247
4248 fn distributed(&mut self, subject: &Expr, op: RelOp, negated: bool) -> R<Option<Cond>> {
4251 let start = self.at;
4252 if !self.accept(&Tok::LParen) {
4253 return Ok(None);
4254 }
4255 let mut logical = false;
4256 let mut any = self.distributed_all(subject, op, negated, &mut logical)?;
4257 while self.accept_word("OR") {
4258 logical = true;
4259 any = Cond::Or(Box::new(any), Box::new(self.distributed_all(subject, op, negated, &mut logical)?));
4260 }
4261 if !logical || !self.accept(&Tok::RParen) {
4262 self.at = start;
4263 return Ok(None);
4264 }
4265 Ok(Some(any))
4266 }
4267
4268 fn distributed_all(&mut self, subject: &Expr, op: RelOp, negated: bool, logical: &mut bool) -> R<Cond> {
4269 let mut all = self.distributed_object(subject, op, negated)?;
4270 while self.accept_word("AND") {
4271 *logical = true;
4272 all = Cond::And(Box::new(all), Box::new(self.distributed_object(subject, op, negated)?));
4273 }
4274 Ok(all)
4275 }
4276
4277 fn distributed_object(&mut self, subject: &Expr, op: RelOp, negated: bool) -> R<Cond> {
4278 if self.accept_word("NOT") {
4279 return Ok(Cond::Not(Box::new(self.distributed_object(subject, op, negated)?)));
4280 }
4281 let start = self.at;
4282 match self.distributed(subject, op, negated) {
4283 Ok(Some(group)) => return Ok(group),
4284 _ => self.at = start,
4285 }
4286 let object = self.expr()?;
4287 Ok(relation(subject.clone(), op, negated, object))
4288 }
4289
4290 fn abbreviation_context(&self, start: usize) -> bool {
4293 for t in self.tokens[..start].iter().rev() {
4294 match &t.tok {
4295 Tok::LParen => {}
4296 Tok::Word(w) if w == "NOT" => {}
4297 Tok::Word(w) => return w == "OR" || w == "AND",
4298 _ => return false,
4299 }
4300 }
4301 false
4302 }
4303
4304 fn relop(&mut self) -> R<Option<RelOp>> {
4305 if self.peek() == Some(&Tok::Lt) && self.peek_at(1) == Some(&Tok::Gt) {
4306 return Err(crate::messages::IWS0065.at(self.pos(), "<> is not an Enterprise COBOL relational operator: it writes NOT ="));
4307 }
4308 let op = match self.peek() {
4309 Some(Tok::Eq) => RelOp::Eq,
4310 Some(Tok::Lt) => RelOp::Lt,
4311 Some(Tok::Gt) => RelOp::Gt,
4312 Some(Tok::Le) => RelOp::Le,
4313 Some(Tok::Ge) => RelOp::Ge,
4314 Some(Tok::Word(w)) if w == "EQUAL" => {
4315 self.at += 1;
4316 self.accept_word("TO");
4317 return Ok(Some(RelOp::Eq));
4318 }
4319 Some(Tok::Word(w)) if w == "GREATER" || w == "LESS" => {
4320 let greater = w == "GREATER";
4321 self.at += 1;
4322 self.accept_word("THAN");
4323 let or_equal = self.accept_word("OR");
4324 if or_equal {
4325 self.expect_word("EQUAL")?;
4326 self.accept_word("TO");
4327 }
4328 return Ok(Some(match (greater, or_equal) {
4329 (true, false) => RelOp::Gt,
4330 (true, true) => RelOp::Ge,
4331 (false, false) => RelOp::Lt,
4332 (false, true) => RelOp::Le,
4333 }));
4334 }
4335 _ => return Ok(None),
4336 };
4337 self.at += 1;
4338 Ok(Some(op))
4339 }
4340}
4341
4342fn host_variables(sql: &str, pos: Pos, longest: Option<usize>) -> Vec<Ref> {
4345 let chars: Vec<char> = sql.chars().collect();
4346 let (mut out, mut i, mut quote) = (Vec::new(), 0, None);
4347 while i < chars.len() {
4348 let c = chars[i];
4349 match quote {
4350 Some(q) if c == q => quote = None,
4351 Some(_) => {}
4352 None if c == '\'' || c == '"' => quote = Some(c),
4353 None if c == ':' && chars.get(i + 1).is_some_and(|n| n.is_ascii_alphanumeric()) => {
4354 let start = i + 1;
4355 let mut end = start;
4356 while end < chars.len() && (chars[end].is_ascii_alphanumeric() || matches!(chars[end], '-' | '_' | '.')) {
4357 end += 1;
4358 }
4359 let path: String = chars[start..end].iter().collect::<String>().to_ascii_uppercase();
4360 let mut parts: Vec<String> = path.trim_end_matches('.').split('.').map(|part| part.chars().take(longest.unwrap_or(usize::MAX)).collect()).collect();
4361 let name = parts.pop().unwrap_or_default();
4362 parts.reverse();
4363 out.push(Ref { name, qualifiers: parts, subscripts: Vec::new(), refmod: None, pos });
4364 i = end;
4365 continue;
4366 }
4367 None => {}
4368 }
4369 i += 1;
4370 }
4371 out
4372}
4373
4374fn cics_options(body: &str) -> Vec<(String, Option<ExecArg>)> {
4376 let chars: Vec<char> = body.chars().collect();
4377 let (mut out, mut i) = (Vec::new(), 0);
4378 while i < chars.len() {
4379 if chars[i].is_whitespace() {
4380 i += 1;
4381 continue;
4382 }
4383 let start = i;
4384 while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '(' {
4385 i += 1;
4386 }
4387 let name: String = chars[start..i].iter().collect::<String>().to_ascii_uppercase();
4388 let mut j = i;
4389 while j < chars.len() && chars[j].is_whitespace() {
4390 j += 1;
4391 }
4392 if j < chars.len() && chars[j] == '(' {
4393 let (mut depth, mut quote, mut k) = (0, None, j);
4394 while k < chars.len() {
4395 match (quote, chars[k]) {
4396 (Some(q), c) if c == q => quote = None,
4397 (Some(_), _) => {}
4398 (None, '\'' | '"') => quote = Some(chars[k]),
4399 (None, '(') => depth += 1,
4400 (None, ')') => {
4401 depth -= 1;
4402 if depth == 0 {
4403 break;
4404 }
4405 }
4406 _ => {}
4407 }
4408 k += 1;
4409 }
4410 let arg: String = chars[j + 1..k.min(chars.len())].iter().collect();
4411 out.push((name, Some(ExecArg::Text(arg.trim().to_owned()))));
4412 i = (k + 1).min(chars.len());
4413 } else {
4414 out.push((name, None));
4415 }
4416 }
4417 out
4418}
4419
4420fn operand_of(text: &str, pos: Pos, extended: bool) -> Option<Operand> {
4423 let source = crate::source::Source { text: text.to_owned(), positions: vec![pos; text.chars().count()], options: Vec::new(), debugging: None, free: Vec::new(), notes: Vec::new() };
4424 let compliance = if extended { numeric::Compliance::Extended } else { numeric::Compliance::Strict };
4425 let tokens = crate::lexer::lex_under(&source, compliance).ok()?;
4426 let mut p = Parser::new(&tokens);
4427 let op = p.operand().ok()?;
4428 (p.at == tokens.len()).then_some(op)
4429}
4430
4431fn system_entries(member: &str) -> R<Vec<DataEntry>> {
4433 system_text_entries(&crate::system::member(member).unwrap_or_default())
4434}
4435
4436fn system_text_entries(text: &str) -> R<Vec<DataEntry>> {
4437 let source = crate::source::read(text)?;
4438 let tokens = crate::lexer::lex(&source)?;
4439 Parser::new(&tokens).data_entries()
4440}
4441
4442const SELECT_CLAUSES: &[&str] = &[
4444 "ASSIGN", "ORGANIZATION", "ACCESS", "FILE", "STATUS", "RECORD", "ALTERNATE", "RELATIVE", "LINE", "SEQUENTIAL", "INDEXED", "RESERVE",
4445 "PADDING", "LOCK", "SHARING", "PASSWORD",
4446];
4447
4448const FD_WORDS: &[&str] = &[
4450 "RECORDING", "RECORD", "BLOCK", "LABEL", "DATA", "VALUE", "CODE-SET", "LINAGE", "REPORT", "REPORTS", "IS", "EXTERNAL", "GLOBAL", "STYLE",
4451];
4452
4453fn is_clause_word(w: &str) -> bool {
4454 matches!(
4455 w,
4456 "PIC" | "PICTURE" | "USAGE" | "VALUE" | "VALUES" | "REDEFINES" | "OCCURS" | "SIGN" | "LEADING" | "TRAILING" | "JUSTIFIED"
4457 | "JUST" | "SYNC" | "SYNCHRONIZED" | "GLOBAL" | "EXTERNAL" | "BLANK" | "VOLATILE"
4458 ) || usage_word(w).is_some()
4459}
4460
4461#[cfg(test)]
4462mod tests {
4463 use super::*;
4464
4465 fn program(body: &str) -> Program {
4466 let text = format!(
4467 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n{body}"
4468 );
4469 crate::parse(&text).unwrap_or_else(|e| panic!("{e}"))
4470 }
4471
4472 #[test]
4473 fn dfhresp_and_dfhvalue_fold_to_the_numbers_in_ibms_tables() {
4474 let body = |operand: &str| format!(" 01 A PIC S9(8) COMP.\n PROCEDURE DIVISION.\n IF A = {operand}\n GOBACK\n END-IF.\n");
4475 let folds_to = |operand: &str, n: i32| {
4476 let p = program(&body(operand));
4477 assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains(&format!("Number(\"{n}\")")), "{operand}");
4478 };
4479 folds_to("DFHRESP(NOTFINISHED)", 113);
4480 folds_to("DFHRESP(DSIDERR)", 12);
4481 folds_to("DFHRESP(FILENOTFOUND)", 12);
4482 assert_eq!(rt::cics_tables::cvda("ENABLED"), Some(23));
4483 folds_to("DFHVALUE(ENABLED)", 23);
4484 for (operand, message) in [("DFHRESP(NOSUCH)", "DFHRESP(NOSUCH): not a CICS condition"), ("DFHVALUE(NOSUCH)", "DFHVALUE(NOSUCH): not a CVDA")] {
4485 let err = crate::parse(&format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n{}", body(operand))).unwrap_err();
4486 assert!(err.to_string().contains(message), "{err}");
4487 }
4488 }
4489
4490 #[test]
4491 fn an_exec_cics_program_compares_with_dfhresp_notfinished() {
4492 program(" 01 WS-RESP PIC S9(8) COMP.\n PROCEDURE DIVISION.\n EXEC CICS RETURN END-EXEC\n IF WS-RESP = DFHRESP(NOTFINISHED)\n GOBACK\n END-IF.\n");
4493 }
4494
4495 #[test]
4496 fn data_entries_with_clauses_in_any_order() {
4497 let p = program(
4498 " 01 G.\n 05 A PIC S9(3)V99 COMP-3 VALUE -1.5.\n 05 B REDEFINES A PIC X(3).\n 05 FILLER PIC X(2) VALUE SPACES.\n 05 T PIC 9 OCCURS 3 TIMES.\n PROCEDURE DIVISION.\n GOBACK.\n",
4499 );
4500 let ws = &p.working_storage;
4501 assert_eq!(ws.len(), 5);
4502 assert_eq!((ws[1].picture.as_deref(), ws[1].usage, &ws[1].value), (Some("S9(3)V99"), Some(Usage::Packed), &Some(Literal::Number("-1.5".into()))));
4503 assert_eq!(ws[2].redefines.as_deref(), Some("A"));
4504 assert_eq!((ws[3].name.as_deref(), &ws[3].value), (None, &Some(Literal::Figurative(Figurative::Space))));
4505 assert_eq!(ws[4].occurs, Some(3));
4506 }
4507
4508 #[test]
4509 fn external_and_global_are_kept_and_a_contained_program_sees_its_containers_global_records() {
4510 let text = [
4511 " IDENTIFICATION DIVISION.\n PROGRAM-ID. A.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
4512 " SELECT F ASSIGN TO FDD.\n SELECT H ASSIGN TO HDD.\n DATA DIVISION.\n FILE SECTION.\n",
4513 " FD F IS GLOBAL IS EXTERNAL.\n 01 F-REC PIC X.\n FD H.\n 01 H-REC PIC X.\n",
4514 " WORKING-STORAGE SECTION.\n 01 X IS EXTERNAL PIC X.\n 01 G IS GLOBAL.\n 05 G1 PIC X.\n 01 L PIC X.\n",
4515 " PROCEDURE DIVISION.\n GOBACK.\n",
4516 " IDENTIFICATION DIVISION.\n PROGRAM-ID. B.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n 01 BG PIC X GLOBAL.\n PROCEDURE DIVISION.\n GOBACK.\n",
4517 " IDENTIFICATION DIVISION.\n PROGRAM-ID. C.\n PROCEDURE DIVISION.\n GOBACK.\n",
4518 " END PROGRAM C.\n END PROGRAM B.\n END PROGRAM A.\n",
4519 ]
4520 .concat();
4521 let all = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
4522 let a = &all[0];
4523 assert!(a.working_storage[0].external && !a.working_storage[0].global && a.working_storage[1].global && !a.working_storage[2].global);
4524 assert!(a.files[0].external && a.files[0].global && a.files[0].records[0].external && a.files[0].records[0].global && !a.files[1].global);
4525 let containers = |p: &Program| p.containers.iter().map(|c| (c.id.clone(), c.working_storage.iter().filter_map(|e| e.name.clone()).collect::<Vec<_>>(), c.files.len())).collect::<Vec<_>>();
4526 assert_eq!(containers(&all[1]), [("A".to_owned(), vec!["G".to_owned(), "G1".to_owned()], 1)]);
4527 assert_eq!(containers(&all[2]), [("B".to_owned(), vec!["BG".to_owned()], 0), ("A".to_owned(), vec!["G".to_owned(), "G1".to_owned()], 1)]);
4528 let on_record = text.replace(" 01 F-REC PIC X.", " 01 F-REC PIC X EXTERNAL.");
4529 let err = crate::parse_all_with(&on_record, &crate::copy::Libraries::default()).unwrap_err();
4530 assert!(err.message.contains("FD F: EXTERNAL goes on the FD, not on a record of the FILE SECTION"), "{err}");
4531 }
4532
4533 #[test]
4534 fn paragraphs_and_nested_if() {
4535 let p = program(
4536 " 01 A PIC X.\n PROCEDURE DIVISION.\n MAIN-LINE.\n PERFORM P2\n GOBACK.\n P2.\n IF A < 'B'\n MOVE 'L' TO A\n ELSE\n IF A = 'B'\n MOVE 'E' TO A\n ELSE\n MOVE 'G' TO A\n END-IF\n END-IF.\n",
4537 );
4538 assert_eq!(p.paragraphs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["MAIN-LINE", "P2"]);
4539 let Stmt::If { otherwise, .. } = &p.paragraphs[1].statements[0] else { panic!() };
4540 assert!(matches!(otherwise[0], Stmt::If { .. }));
4541 }
4542
4543 #[test]
4544 fn a_program_names_the_programs_it_directly_contains() {
4545 let program = |id: &str, inner: &str| format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. {id}.\n PROCEDURE DIVISION.\n GOBACK.\n{inner} END PROGRAM {id}.\n");
4546 let text = program("OUTER", &[program("A", &program("A1", "")), program("B", "")].concat());
4547 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
4548 let contained: Vec<(&str, &[String])> = all.iter().map(|p| (p.id.as_str(), p.nested.as_slice())).collect();
4549 assert_eq!(contained, [("OUTER", &["A".to_owned(), "B".to_owned()][..]), ("A", &["A1".to_owned()][..]), ("A1", &[][..]), ("B", &[][..])]);
4550 let text = program("OUTER", &[program("A IS COMMON", &program("A1 COMMON INITIAL", "")), program("B", "")].concat()).replace("END PROGRAM A IS COMMON", "END PROGRAM A").replace("END PROGRAM A1 COMMON INITIAL", "END PROGRAM A1");
4551 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
4552 let common: Vec<(&str, bool, bool)> = all.iter().map(|p| (p.id.as_str(), p.common, p.initial)).collect();
4553 assert_eq!(common, [("OUTER", false, false), ("A", true, false), ("A1", true, true), ("B", false, false)]);
4554 }
4555
4556 #[test]
4557 fn each_program_carries_the_messages_of_its_own_source_and_not_its_contained_programs() {
4558 let program = |id: &str, name: &str, inner: &str| {
4559 format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. {id}.\n PROCEDURE DIVISION.\n DISPLAY {name}\n GOBACK.\n{inner} END PROGRAM {id}.\n")
4560 };
4561 let text = [program("O#1", "O1", &[program("A", "A@1", ""), program("B", "B1", "")].concat()), program("NEXT", "N%1", "")].concat();
4562 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
4563 let lines: Vec<(&str, Vec<u32>)> = all.iter().map(|p| (p.id.as_str(), p.messages.iter().map(|m| m.pos.line).collect())).collect();
4564 assert_eq!(lines, [("O#1", vec![2, 18]), ("A", vec![9]), ("B", vec![]), ("NEXT", vec![22])]);
4565 }
4566
4567 #[test]
4568 fn inline_perform_varying_with_a_compound_condition() {
4569 let p = program(
4570 " 01 I PIC 9(4) COMP.\n 01 J PIC 9(4) COMP.\n PROCEDURE DIVISION.\n PERFORM VARYING J FROM 0 BY 1\n UNTIL J > 31 OR I + J > 64\n CONTINUE\n END-PERFORM.\n",
4571 );
4572 let Stmt::PerformInline { repeat: Loop::Varying { varying, .. }, .. } = &p.paragraphs[0].statements[0] else { panic!() };
4573 assert!(matches!(varying.until, Cond::Or(..)));
4574 }
4575
4576 #[test]
4577 fn functions_reference_modification_and_length_of() {
4578 let p = program(
4579 " 01 X PIC X(9).\n 01 N PIC 9(4) COMP.\n PROCEDURE DIVISION.\n COMPUTE N = FUNCTION ORD(X(N + 1:1)) - 1\n MOVE FUNCTION NATIONAL-OF(X, 1047) TO X\n MOVE LENGTH OF X TO N.\n",
4580 );
4581 let Stmt::Compute { expr: Expr::Bin(left, BinOp::Sub, _), .. } = &p.paragraphs[0].statements[0] else { panic!() };
4582 let Expr::Operand(Operand::Function(f)) = left.as_ref() else { panic!() };
4583 let Expr::Operand(Operand::Ref(r)) = &f.args[0] else { panic!() };
4584 assert!(r.refmod.is_some());
4585 assert!(matches!(&p.paragraphs[0].statements[2], Stmt::Move { from: Operand::LengthOf(_), .. }));
4586 }
4587
4588 #[test]
4589 fn divide_giving_remainder_and_add_to() {
4590 let p = program(
4591 " 01 N PIC 9(4) COMP.\n 01 H PIC 9(4) COMP.\n 01 L PIC 9(4) COMP.\n PROCEDURE DIVISION.\n DIVIDE N BY 16 GIVING H REMAINDER L\n ADD 1 TO H ROUNDED.\n",
4592 );
4593 let Stmt::Arith(a) = &p.paragraphs[0].statements[0] else { panic!() };
4594 assert_eq!(a.verb, ArithVerb::Divide);
4595 assert!(a.remainder.is_some());
4596 let Stmt::Arith(add) = &p.paragraphs[0].statements[1] else { panic!() };
4597 assert!(add.computations[0].0.rounded);
4598 }
4599
4600 #[test]
4601 fn abbreviated_combined_relation() {
4602 let p = program(" 01 A PIC 9.\n PROCEDURE DIVISION.\n IF A = 1 OR 2 CONTINUE END-IF.\n");
4603 let Stmt::If { cond: Cond::Or(_, right), .. } = &p.paragraphs[0].statements[0] else { panic!() };
4604 assert!(matches!(right.as_ref(), Cond::Rel(_, RelOp::Eq, _)));
4605 }
4606
4607 #[test]
4608 fn indexed_select_clauses_and_keyed_statements() {
4609 let text = [
4610 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
4611 " SELECT K ASSIGN TO KDD ORGANIZATION IS INDEXED\n",
4612 " ACCESS MODE IS DYNAMIC RECORD KEY IS K-ID\n",
4613 " ALTERNATE KEY K-ALT WITH DUPLICATES.\n",
4614 " SELECT N ASSIGN TO NDD STATUS N-FS N-VS ORGANIZATION\n",
4615 " INDEXED FILE STATUS IS N-FS RECORD N-ID.\n",
4616 " DATA DIVISION.\n FILE SECTION.\n FD K.\n 01 K-REC.\n 05 K-ID PIC XX.\n 05 K-ALT PIC XX.\n",
4617 " FD N.\n 01 N-ID PIC XX.\n WORKING-STORAGE SECTION.\n 01 N-FS PIC XX.\n 01 N-VS PIC X(6).\n",
4618 " PROCEDURE DIVISION.\n",
4619 " READ K NEXT RECORD AT END CONTINUE\n",
4620 " NOT AT END CONTINUE END-READ\n",
4621 " READ K KEY IS K-ALT INVALID KEY CONTINUE END-READ\n",
4622 " START K KEY IS NOT LESS THAN K-ID\n",
4623 " INVALID KEY CONTINUE NOT INVALID KEY CONTINUE\n",
4624 " END-START\n",
4625 " REWRITE K-REC INVALID KEY CONTINUE END-REWRITE\n",
4626 " DELETE K RECORD END-DELETE.\n",
4627 ]
4628 .concat();
4629 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
4630 let f = &p.files[0];
4631 assert_eq!((f.organization, f.access), (Organization::Indexed, Access::Dynamic));
4632 assert_eq!(f.record_key.as_ref().map(|r| r.name.as_str()), Some("K-ID"));
4633 assert_eq!(f.alternate_keys.iter().map(|(r, d)| (r.name.as_str(), *d)).collect::<Vec<_>>(), [("K-ALT", true)]);
4634 let n = &p.files[1];
4635 assert_eq!((n.organization, n.record_key.as_ref().map(|r| r.name.as_str())), (Organization::Indexed, Some("N-ID")));
4636 let s = &p.paragraphs[0].statements;
4637 let Stmt::Read(r) = &s[0] else { panic!() };
4638 assert!(r.next && !r.previous && r.at_end.on.is_some() && r.at_end.not_on.is_some());
4639 let Stmt::Read(r) = &s[1] else { panic!() };
4640 assert!(r.key.is_some() && r.invalid.on.is_some() && !r.next);
4641 let Stmt::Start { key: Some((RelOp::Ge, _)), invalid, .. } = &s[2] else { panic!() };
4642 assert!(invalid.on.is_some() && invalid.not_on.is_some());
4643 assert!(matches!(&s[3], Stmt::Rewrite { invalid, .. } if invalid.on.is_some()));
4644 assert!(matches!(&s[4], Stmt::Delete { .. }));
4645 }
4646
4647 #[test]
4648 fn entry_alter_the_go_to_forms_and_section_priorities() {
4649 let p = program(
4650 " 01 D PIC 9.\n LINKAGE SECTION.\n 01 L PIC X.\n PROCEDURE DIVISION.\n S SECTION 50.\n P1.\n ENTRY 'Alt' USING BY VALUE L.\n ALTER P2 TO PROCEED TO P1 P3 TO P1\n GO TO P1 P2 DEPENDING ON D.\n P2.\n GO TO.\n P3.\n GO TO P1.\n T SECTION.\n",
4651 );
4652 let priorities: Vec<(&str, u8)> = p.paragraphs.iter().map(|q| (q.name.as_str(), q.priority)).collect();
4653 assert_eq!(priorities, [("S", 50), ("P1", 50), ("P2", 50), ("P3", 50), ("T", 0)]);
4654 let s = &p.paragraphs[1].statements;
4655 assert!(matches!(&s[0], Stmt::Entry { name, using, .. } if name == "ALT" && using == &[Param { by_value: true, name: "L".into() }]));
4656 assert!(matches!(&s[2], Stmt::Alter { pairs, .. } if pairs.len() == 2 && pairs[1].0.name == "P3"));
4657 assert!(matches!(&s[3], Stmt::GoToDepending { targets, on, .. } if targets.len() == 2 && on.name == "D"));
4658 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::GoTo { target: None, .. }));
4659 assert!(matches!(&p.paragraphs[3].statements[0], Stmt::GoTo { target: Some(t), .. } if t.name == "P1"));
4660 }
4661
4662 #[test]
4663 fn by_left_out_stop_literal_and_a_phrase_that_belongs_to_the_outer_statement() {
4664 let p = program(
4665 " 01 A PIC 9.\n PROCEDURE DIVISION.\n CALL 'P' USING CONTENT A REFERENCE A VALUE A A\n ON EXCEPTION ADD 1 TO A\n NOT ON EXCEPTION ADD 2 TO A\n END-CALL\n STOP 'OPERATOR'\n STOP ZERO\n STOP RUN.\n",
4666 );
4667 let s = &p.paragraphs[0].statements;
4668 let Stmt::Call(c) = &s[0] else { panic!("{:?}", s[0]) };
4669 let modes: Vec<ArgMode> = c.using.iter().map(|a| a.mode).collect();
4670 assert_eq!(modes, [ArgMode::Content, ArgMode::Reference, ArgMode::Value, ArgMode::Value]);
4671 assert!(c.on_exception.as_ref().is_some_and(|b| matches!(&b[0], Stmt::Arith(a) if a.size_error.is_none())) && c.not_on_exception.is_some());
4672 assert!(matches!(&s[1], Stmt::Display { items, .. } if items == &[Operand::Literal(Literal::Alnum("OPERATOR".into()))]));
4673 assert!(matches!(&s[2], Stmt::Display { .. }) && matches!(&s[3], Stmt::StopRun { .. }));
4674 }
4675
4676 #[test]
4677 fn select_clauses_with_their_optional_words_left_out() {
4678 let text = [
4679 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
4680 " SELECT R ASSIGN RDD ORGANIZATION RELATIVE ACCESS RANDOM\n RELATIVE R-KEY.\n",
4681 " SELECT Q ASSIGN QDD RELATIVE ACCESS DYNAMIC\n RELATIVE IS Q-KEY.\n",
4682 " SELECT X ASSIGN XDD ORGANIZATION INDEXED\n RECORD X-KEY ALTERNATE RECORD X-ALT\n ALTERNATE RECORD IS X-ALT2 WITH DUPLICATES.\n",
4683 " SELECT S ASSIGN SDD RECORD DELIMITER IS STANDARD-1.\n",
4684 " DATA DIVISION.\n FILE SECTION.\n FD R.\n 01 R-REC PIC X.\n FD Q.\n 01 Q-REC PIC X.\n",
4685 " FD X.\n 01 X-REC.\n 05 X-KEY PIC X.\n 05 X-ALT PIC X.\n 05 X-ALT2 PIC X.\n FD S.\n 01 S-REC PIC X.\n",
4686 " WORKING-STORAGE SECTION.\n 01 R-KEY PIC 9.\n 01 Q-KEY PIC 9.\n PROCEDURE DIVISION.\n GOBACK.\n",
4687 ]
4688 .concat();
4689 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
4690 let f = &p.files;
4691 assert!(f[0].organization == Organization::Relative && f[0].relative_key.as_ref().is_some_and(|k| k.name == "R-KEY"));
4692 assert!(f[1].organization == Organization::Relative && f[1].relative_key.as_ref().is_some_and(|k| k.name == "Q-KEY"));
4693 let alternates: Vec<(&str, bool)> = f[2].alternate_keys.iter().map(|(k, d)| (k.name.as_str(), *d)).collect();
4694 assert_eq!(alternates, [("X-ALT", false), ("X-ALT2", true)]);
4695 assert!(f[3].record_key.is_none() && f[3].organization == Organization::Sequential);
4696 }
4697
4698 #[test]
4699 fn assign_keeps_its_first_name_and_password_is_read() {
4700 let text = [
4701 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
4702 " SELECT F ASSIGN TO UT-S-FDD XXXXX044 'B.DAT'\n ORGANIZATION SEQUENTIAL.\n",
4703 " SELECT K ASSIGN KDD ORGANIZATION INDEXED\n RECORD KEY IS KK PASSWORD IS PW\n ALTERNATE RECORD KA PASSWORD PW.\n",
4704 " DATA DIVISION.\n FILE SECTION.\n FD F.\n 01 R PIC X.\n FD K.\n 01 KR.\n 05 KK PIC X.\n 05 KA PIC X.\n",
4705 " WORKING-STORAGE SECTION.\n 01 PW PIC X(8).\n PROCEDURE DIVISION.\n GOBACK.\n",
4706 ]
4707 .concat();
4708 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
4709 assert_eq!((p.files[0].assign.as_str(), p.files[0].organization), ("FDD", Organization::Sequential));
4710 assert_eq!(p.files[1].alternate_keys.iter().map(|(k, _)| k.name.as_str()).collect::<Vec<_>>(), ["KA"]);
4711 }
4712
4713 #[test]
4714 fn procedure_names_of_digits_and_a_subscripted_times_count() {
4715 let p = program(
4716 " 01 T.\n 05 N PIC 9 OCCURS 2.\n 01 I PIC 9.\n PROCEDURE DIVISION.\n 00 SECTION 00.\n 10.\n PERFORM 20 N (I) TIMES\n PERFORM 20 THRU 30\n PERFORM N (1) TIMES CONTINUE END-PERFORM\n PERFORM TEST BEFORE UNTIL I > 1 ADD 1 TO I END-PERFORM\n GO TO 20 30 DEPENDING ON I\n ALTER 30 TO PROCEED TO 20 40 TO 20.\n 20.\n PERFORM 00.\n 30.\n GO TO 20.\n 40.\n GO TO 20.\n",
4717 );
4718 let names: Vec<&str> = p.paragraphs.iter().map(|q| q.name.as_str()).collect();
4719 assert_eq!(names, ["00", "10", "20", "30", "40"]);
4720 let s = &p.paragraphs[1].statements;
4721 assert!(matches!(&s[0], Stmt::PerformProc { from, repeat: Loop::Times(Expr::Operand(Operand::Ref(r))), .. } if from.name == "20" && r.name == "N" && r.subscripts.len() == 1));
4722 assert!(matches!(&s[1], Stmt::PerformProc { from, thru: Some(t), repeat: Loop::Once, .. } if from.name == "20" && t.name == "30"));
4723 assert!(matches!(&s[2], Stmt::PerformInline { repeat: Loop::Times(_), .. }));
4724 assert!(matches!(&s[3], Stmt::PerformInline { repeat: Loop::Until { test_after: false, .. }, .. }));
4725 assert!(matches!(&s[4], Stmt::GoToDepending { targets, .. } if targets.len() == 2 && targets[1].name == "30"));
4726 assert!(matches!(&s[5], Stmt::Alter { pairs, .. } if pairs.len() == 2 && pairs[1].0.name == "40"));
4727 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::PerformProc { from, .. } if from.name == "00"));
4728 }
4729
4730 #[test]
4731 fn corresponding_forms_take_one_receiving_group() {
4732 let p = program(
4733 " PROCEDURE DIVISION.\n MOVE CORR A TO B(1)\n ADD CORRESPONDING A TO B ROUNDED\n ON SIZE ERROR CONTINUE END-ADD\n SUBTRACT CORR A FROM B.\n",
4734 );
4735 let s = &p.paragraphs[0].statements;
4736 let Stmt::Corresponding(m) = &s[0] else { panic!("{:?}", s[0]) };
4737 assert_eq!((m.verb, m.from.name.as_str(), m.to.name.as_str(), m.to.subscripts.len()), (CorrespondingVerb::Move, "A", "B", 1));
4738 let Stmt::Corresponding(a) = &s[1] else { panic!() };
4739 assert!(a.verb == CorrespondingVerb::Add && a.rounded && a.size_error.is_some());
4740 assert!(matches!(&s[2], Stmt::Corresponding(c) if c.verb == CorrespondingVerb::Subtract && !c.rounded));
4741 let text = " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n PROCEDURE DIVISION.\n MOVE CORRESPONDING A TO B C.\n";
4742 assert!(crate::parse(text).unwrap_err().message.contains("one receiving group"));
4743 }
4744
4745 #[test]
4746 fn upsi_switch_entries_take_each_form_and_reach_contained_programs() {
4747 let text = [
4748 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
4749 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n UPSI-0 IS ABBREV-SWITCH\n",
4750 " ON ON-SWITCH OFF IS OFF-SWITCH\n UPSI-1 OFF STATUS IS F1 ON T1\n",
4751 " UPSI-7 SW-7 C01 IS TOP.\n PROCEDURE DIVISION.\n",
4752 " SET ABBREV-SWITCH SW-7 TO ON SW-7 TO OFF.\n",
4753 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n GOBACK.\n",
4754 " END PROGRAM INNER.\n END PROGRAM OUTER.\n",
4755 ]
4756 .concat();
4757 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
4758 let (outer, inner) = (&programs[0], &programs[1]);
4759 let entry = |p: &Program, k: usize| {
4760 let s = &p.environment.switches[k];
4761 format!("{} {:?} {:?} {:?}", s.number, s.mnemonic, s.on, s.off)
4762 };
4763 let expected = [r#"0 Some("ABBREV-SWITCH") Some("ON-SWITCH") Some("OFF-SWITCH")"#, r#"1 None Some("T1") Some("F1")"#, r#"7 Some("SW-7") None None"#];
4764 assert_eq!((0..3).map(|k| entry(outer, k)).collect::<Vec<_>>(), expected);
4765 assert_eq!((0..3).map(|k| entry(inner, k)).collect::<Vec<_>>(), expected);
4766 assert_eq!(outer.environment.mnemonics, [("TOP".to_owned(), "C01".to_owned())]);
4767 let Stmt::Set { set: SetStmt::Switches(groups), .. } = &outer.paragraphs[0].statements[0] else { panic!("{:?}", outer.paragraphs[0].statements) };
4768 let names: Vec<(Vec<&str>, bool)> = groups.iter().map(|(t, on)| (t.iter().map(|r| r.name.as_str()).collect(), *on)).collect();
4769 assert_eq!(names, [(vec!["ABBREV-SWITCH", "SW-7"], true), (vec!["SW-7"], false)]);
4770 }
4771
4772 #[test]
4773 fn an_upsi_switch_entry_names_something_and_each_status_once() {
4774 let entry = |clause: &str| {
4775 let text = format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n ENVIRONMENT DIVISION.\n CONFIGURATION SECTION.\n SPECIAL-NAMES.\n {clause}.\n PROCEDURE DIVISION.\n GOBACK.\n");
4776 crate::parse(&text).err().map(|e| e.message)
4777 };
4778 assert_eq!(entry("UPSI-2"), Some("UPSI-2: a mnemonic-name or an ON or OFF STATUS phrase must follow it".into()));
4779 assert_eq!(entry("UPSI-2 ON A ON B"), Some("UPSI-2: a second ON STATUS phrase".into()));
4780 assert_eq!(entry("UPSI-2 IS S OFF A"), None);
4781 }
4782
4783 #[test]
4784 fn advancing_mnemonic_names_reach_contained_programs_and_linage_is_noted() {
4785 let text = [
4786 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
4787 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n C01 IS TOP-OF-PAGE CSP NO-SPACE\n",
4788 " AFP-5A IS PAGE-MODE UPSI-0 IS SWITCH-0 ON STATUS IS SW-ON.\n",
4789 " INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n SELECT P ASSIGN TO PDD.\n",
4790 " DATA DIVISION.\n FILE SECTION.\n FD P LINAGE IS 60.\n 01 P-REC PIC X.\n",
4791 " PROCEDURE DIVISION.\n WRITE P-REC AFTER TOP-OF-PAGE\n",
4792 " WRITE P-REC BEFORE ADVANCING NO-SPACE\n WRITE P-REC AFTER ADVANCING PAGE-COUNT LINES.\n",
4793 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n",
4794 " WRITE P-REC AFTER ADVANCING PAGE-MODE.\n END PROGRAM INNER.\n END PROGRAM OUTER.\n",
4795 ]
4796 .concat();
4797 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
4798 let (outer, inner) = (&programs[0], &programs[1]);
4799 let pairs = |p: &Program| p.environment.mnemonics.iter().map(|(n, e)| format!("{n}={e}")).collect::<Vec<_>>();
4800 assert_eq!(pairs(outer), ["TOP-OF-PAGE=C01", "NO-SPACE=CSP", "PAGE-MODE=AFP-5A"]);
4801 assert_eq!(outer.files[0].linage.as_ref().map(|l| &l.lines), Some(&LinageValue::Integer("60".into())));
4802 let advancing = |p: &Program, i: usize| match &p.paragraphs[0].statements[i] {
4803 Stmt::Write { advancing: Some(a), .. } => a.clone(),
4804 other => panic!("{other:?}"),
4805 };
4806 assert!(matches!(advancing(outer, 0), Advancing::Mnemonic { before: false, environment, .. } if environment == "C01"));
4807 assert!(matches!(advancing(outer, 1), Advancing::Mnemonic { before: true, environment, .. } if environment == "CSP"));
4808 assert!(matches!(advancing(outer, 2), Advancing::Lines { before: false, .. }));
4809 assert!(matches!(advancing(inner, 0), Advancing::Mnemonic { environment, .. } if environment == "AFP-5A"));
4810 }
4811
4812 #[test]
4813 fn exit_section_opening_a_paragraph_is_a_statement_not_a_section() {
4814 let p = program(" PROCEDURE DIVISION.\n MAIN-LINE SECTION.\n SKIPPED.\n EXIT SECTION.\n NEVER.\n GOBACK.\n");
4815 let names: Vec<&str> = p.paragraphs.iter().map(|q| q.name.as_str()).collect();
4816 assert_eq!(names, ["MAIN-LINE", "SKIPPED", "NEVER"]);
4817 assert!(matches!(p.paragraphs[1].statements[..], [Stmt::Exit { kind: ExitKind::Section, .. }, Stmt::SentenceEnd]), "{:?}", p.paragraphs[1].statements);
4818 }
4819
4820 fn linage_program(fds: &str, procedure: &str) -> String {
4821 [
4822 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
4823 " SELECT P ASSIGN TO PDD.\n SELECT S ASSIGN TO SDD.\n SELECT Q ASSIGN TO QDD.\n",
4824 " DATA DIVISION.\n FILE SECTION.\n",
4825 fds,
4826 " WORKING-STORAGE SECTION.\n 01 SIZES.\n 05 BODY PIC 99.\n 01 T-M PIC 9.\n PROCEDURE DIVISION.\n",
4827 procedure,
4828 ]
4829 .concat()
4830 }
4831
4832 #[test]
4833 fn linage_phrases_come_in_any_order_and_an_sds_is_dropped() {
4834 let fds = [
4835 " FD P LINAGE IS BODY OF SIZES LINES LINES AT BOTTOM 6\n WITH FOOTING AT 45 TOP T-M.\n 01 P-REC PIC X.\n",
4836 " SD S LINAGE 10.\n 01 S-REC PIC X.\n",
4837 " FD Q LABEL RECORDS STANDARD LINAGE 5 RECORDING MODE F.\n 01 Q-REC PIC X.\n",
4838 ]
4839 .concat();
4840 let p = crate::parse(&linage_program(&fds, " GOBACK.\n")).unwrap_or_else(|e| panic!("{e}"));
4841 let l = p.files[0].linage.as_ref().unwrap();
4842 assert!(matches!(&l.lines, LinageValue::Data(r) if r.name == "BODY" && r.qualifiers == ["SIZES"]));
4843 assert_eq!((&l.footing, &l.bottom), (&Some(LinageValue::Integer("45".into())), &Some(LinageValue::Integer("6".into()))));
4844 assert!(matches!(&l.top, Some(LinageValue::Data(r)) if r.name == "T-M"));
4845 assert_eq!(p.files[1].linage, None);
4846 assert_eq!((p.files[2].linage.as_ref().map(|l| &l.lines), p.files[2].recording), (Some(&LinageValue::Integer("5".into())), Some('F')));
4847 for (fd, expected) in [("LINAGE 5 TOP 1 TOP 2", "TOP is given twice"), ("LINAGE 5.5", "not an unsigned integer"), ("LINAGE 5 FOOTING", "after FOOTING")] {
4848 let text = linage_program(&format!(" FD P {fd}.\n 01 P-REC PIC X.\n"), " GOBACK.\n");
4849 let e = crate::parse(&text).unwrap_err();
4850 assert!(e.message.contains(expected), "{fd}: {}", e.message);
4851 }
4852 }
4853
4854 #[test]
4855 fn record_varying_keeps_from_and_to_apart_and_an_odo_table_its_fewest_occurrences() {
4856 let fds = [
4857 " FD P RECORD IS VARYING IN SIZE TO 80 CHARACTERS.\n 01 P-REC PIC X.\n",
4858 " FD S RECORD VARYING FROM 10.\n 01 S-REC.\n 05 N PIC 9.\n 05 T PIC X OCCURS 9 DEPENDING ON N.\n",
4859 " FD Q RECORD CONTAINS 10 TO 80.\n 01 Q-REC.\n 05 U PIC X OCCURS 2 TO 9 TIMES DEPENDING ON N.\n",
4860 ]
4861 .concat();
4862 let p = crate::parse(&linage_program(&fds, " GOBACK.\n")).unwrap_or_else(|e| panic!("{e}"));
4863 let bounds = |k: usize| (p.files[k].record_varying, p.files[k].record_min, p.files[k].record_max);
4864 assert_eq!([bounds(0), bounds(1), bounds(2)], [(true, None, Some(80)), (true, Some(10), None), (false, Some(10), Some(80))]);
4865 assert_eq!((p.files[1].records[2].occurs, p.files[1].records[2].occurs_min), (Some(9), Some(1)));
4866 assert_eq!((p.files[2].records[1].occurs, p.files[2].records[1].occurs_min), (Some(9), Some(2)));
4867 }
4868
4869 #[test]
4870 fn end_of_page_phrases_leave_a_read_its_not_at_end() {
4871 let procedure = [
4872 " READ Q AT END WRITE P-REC\n",
4873 " NOT AT END WRITE P-REC AT EOP CONTINUE END-WRITE\n END-READ\n",
4874 " WRITE P-REC BEFORE ADVANCING 2 LINES END-OF-PAGE CONTINUE\n",
4875 " NOT AT END-OF-PAGE CONTINUE\n END-WRITE\n",
4876 " WRITE P-REC INVALID KEY CONTINUE NOT EOP CONTINUE.\n",
4877 ]
4878 .concat();
4879 let fds = " FD P LINAGE 5.\n 01 P-REC PIC X.\n FD Q.\n 01 Q-REC PIC X.\n";
4880 let p = crate::parse(&linage_program(fds, &procedure)).unwrap_or_else(|e| panic!("{e}"));
4881 let s = &p.paragraphs[0].statements;
4882 let Stmt::Read(r) = &s[0] else { panic!("{:?}", s[0]) };
4883 let (Some(on), Some(not_on)) = (&r.at_end.on, &r.at_end.not_on) else { panic!("{r:?}") };
4884 assert!(matches!(&on[0], Stmt::Write { end_of_page, .. } if *end_of_page == Handlers::default()));
4885 assert!(matches!(¬_on[0], Stmt::Write { end_of_page, .. } if end_of_page.on.is_some() && end_of_page.not_on.is_none()));
4886 assert!(matches!(&s[1], Stmt::Write { advancing: Some(Advancing::Lines { before: true, .. }), end_of_page, .. } if end_of_page.on.is_some() && end_of_page.not_on.is_some()));
4887 assert!(matches!(&s[2], Stmt::Write { invalid, end_of_page, .. } if invalid.on.is_some() && end_of_page.not_on.is_some()));
4888 }
4889
4890 #[test]
4891 fn decimal_point_is_comma_and_currency_signs_reach_contained_programs() {
4892 let text = [
4893 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
4894 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n CURRENCY SIGN IS 'W'\n",
4895 " CURRENCY 'EUR ' WITH PICTURE SYMBOL 'y'\n DECIMAL-POINT IS COMMA.\n",
4896 " DATA DIVISION.\n WORKING-STORAGE SECTION.\n 01 A PIC 9V9 VALUE 1,5.\n",
4897 " PROCEDURE DIVISION.\n GOBACK.\n",
4898 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n GOBACK.\n",
4899 " END PROGRAM INNER.\n END PROGRAM OUTER.\n",
4900 ]
4901 .concat();
4902 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
4903 assert!(programs.iter().all(|p| p.environment.decimal_point_comma));
4904 assert_eq!(programs[0].working_storage[0].value, Some(Literal::Number("1.5".into())));
4905 let signs = [CurrencySign { value: "W".into(), symbol: 'W', hex: None }, CurrencySign { value: "EUR ".into(), symbol: 'y', hex: None }];
4906 assert!(programs.iter().all(|p| p.environment.currency == signs));
4907 for (clause, why) in [
4908 ("'E'", "one character"),
4909 ("'EUR'", "one character"),
4910 ("'E9' PICTURE SYMBOL 'Y'", "a digit"),
4911 ("'EUR' PICTURE SYMBOL 'Z'", "PICTURE SYMBOL"),
4912 ("X'5B5B'", "X'5B5B' is not one character"),
4913 ("X''", "a nonempty alphanumeric literal"),
4914 ("'EUR' WITH 'Y'", "PICTURE SYMBOL after WITH"),
4915 ("'W'\n CURRENCY 'WON' PICTURE SYMBOL 'W'", "a second CURRENCY SIGN"),
4916 ] {
4917 let bad = text.replace("CURRENCY SIGN IS 'W'", &format!("CURRENCY SIGN IS {clause}"));
4918 let message = crate::parse(&bad).unwrap_err().message;
4919 assert!(message.contains(why), "{clause}: {message}");
4920 }
4921 let hex = text.replace("CURRENCY SIGN IS 'W'", "CURRENCY SIGN IS X'5B'\n CURRENCY X'9F' PICTURE SYMBOL 'Y'\n CURRENCY X'86'");
4922 let mut p = crate::parse(&hex).unwrap_or_else(|e| panic!("{e}"));
4923 let pending = |symbol: char, bytes: &[u8]| CurrencySign { value: String::new(), symbol, hex: Some(bytes.to_vec()) };
4924 assert_eq!(p.environment.currency[..3], [pending(HEX_SYMBOL, &[0x5B]), pending('Y', &[0x9F]), pending(HEX_SYMBOL, &[0x86])]);
4925 let page = |bytes: &[u8]| bytes.iter().map(|b| match b { 0x5B => '$', 0x9F => '€', _ => 'f' }).collect();
4926 decode_currency(&mut p.environment, page).unwrap();
4927 let decoded = |value: &str, symbol: char| CurrencySign { value: value.into(), symbol, hex: None };
4928 assert_eq!(p.environment.currency[..3], [decoded("$", '$'), decoded("€", 'Y'), decoded("f", 'f')]);
4929 }
4930
4931 #[test]
4932 fn a_picture_keeps_the_case_of_a_currency_symbol_and_picture_symbol_is_no_picture_string() {
4933 let text = concat!(
4934 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n CONFIGURATION SECTION.\n",
4935 " SPECIAL-NAMES.\n CURRENCY SIGN 'CHF ' WITH PICTURE SYMBOL 'f'.\n DATA DIVISION.\n",
4936 " WORKING-STORAGE SECTION.\n 01 A PIC fff9v99.\n 01 B PIC zz9.\n",
4937 );
4938 let p = crate::parse(text).unwrap_or_else(|e| panic!("{e}"));
4939 assert_eq!(p.environment.currency, [CurrencySign { value: "CHF ".into(), symbol: 'f', hex: None }]);
4940 assert_eq!((p.working_storage[0].picture.as_deref(), p.working_storage[1].picture.as_deref()), (Some("fff9V99"), Some("ZZ9")));
4941 }
4942
4943 #[test]
4944 fn renames_and_condition_names_with_a_false_value() {
4945 let p = program(concat!(
4946 " 01 R.\n 05 A PIC X.\n 05 B PIC X.\n 88 B-ON VALUE 'Y' FALSE 'N'.\n",
4947 " 88 B-OFF VALUES 'N' 'X' WHEN SET TO FALSE IS SPACE.\n",
4948 " 66 AB RENAMES A THRU B.\n 66 BB RENAMES B OF R.\n",
4949 " PROCEDURE DIVISION.\n SET B-ON B-OFF TO FALSE.\n",
4950 ));
4951 let ws = &p.working_storage;
4952 assert_eq!((ws[3].false_value.as_ref(), ws[3].condition_values.len()), (Some(&Literal::Alnum("N".into())), 1));
4953 assert_eq!((ws[4].false_value.as_ref(), ws[4].condition_values.len()), (Some(&Literal::Figurative(Figurative::Space)), 2));
4954 let (first, last) = ws[5].renames.as_ref().unwrap();
4955 assert_eq!((ws[5].level, first.name.as_str(), last.as_ref().map(|r| r.name.as_str())), (66, "A", Some("B")));
4956 assert_eq!(ws[6].renames.as_ref().unwrap().0.qualifiers, ["R"]);
4957 assert!(matches!(&p.paragraphs[0].statements[0], Stmt::Set { set: SetStmt::ConditionFalse(t), .. } if t.len() == 2));
4958 }
4959
4960 #[test]
4961 fn a_procedure_name_of_digits_alone_is_read_where_a_procedure_name_goes() {
4962 let p = program(concat!(
4963 " 01 D PIC 9.\n PROCEDURE DIVISION.\n 00 SECTION 50.\n 010.\n",
4964 " GO TO 3 010 OF 00 DEPENDING ON D\n PERFORM 3 TIMES DISPLAY D END-PERFORM\n",
4965 " PERFORM 3 THRU 4 2 TIMES\n DISPLAY 1\n 3.\n ALTER 4 TO 3.\n 4.\n GO TO 3.\n",
4966 ));
4967 let names: Vec<(&str, bool, u8)> = p.paragraphs.iter().map(|q| (q.name.as_str(), q.is_section, q.priority)).collect();
4968 assert_eq!(names, [("00", true, 50), ("010", false, 50), ("3", false, 50), ("4", false, 50)]);
4969 let s = &p.paragraphs[1].statements;
4970 assert!(matches!(&s[0], Stmt::GoToDepending { targets, .. } if targets[0].name == "3" && targets[1].section.as_deref() == Some("00")));
4971 assert!(matches!(&s[1], Stmt::PerformInline { repeat: Loop::Times(_), .. }));
4972 assert!(matches!(&s[2], Stmt::PerformProc { from, thru: Some(t), repeat: Loop::Times(_), .. } if from.name == "3" && t.name == "4"));
4973 assert!(matches!(&s[3], Stmt::Display { items, .. } if items.len() == 1));
4974 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::Alter { pairs, .. } if pairs[0].0.name == "4" && pairs[0].1.name == "3"));
4975 }
4976
4977 #[test]
4978 fn record_delimiter_is_for_a_sequential_file() {
4979 let program = |selects: &str| {
4980 let head = " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n";
4981 format!("{head}{selects} DATA DIVISION.\n FILE SECTION.\n FD S.\n 01 S-REC PIC X(80).\n PROCEDURE DIVISION.\n GOBACK.\n")
4982 };
4983 let p = crate::parse(&program(" SELECT S ASSIGN TO SDD RECORD DELIMITER IS STANDARD-1\n ORGANIZATION IS SEQUENTIAL.\n")).unwrap_or_else(|e| panic!("{e}"));
4984 assert_eq!(p.files[0].organization, Organization::Sequential);
4985 crate::parse(&program(" SELECT S ASSIGN TO SDD RECORD DELIMITER TAPEDEL.\n")).unwrap_or_else(|e| panic!("{e}"));
4986 let err = crate::parse(&program(" SELECT S ASSIGN TO SDD ORGANIZATION LINE SEQUENTIAL\n RECORD DELIMITER STANDARD-1.\n")).unwrap_err();
4987 assert_eq!(err.message, "RECORD DELIMITER on S: the clause is for a file of ORGANIZATION SEQUENTIAL");
4988 }
4989 #[test]
4990 fn volatile_is_a_clause_without_effect() {
4991 let p = program(" 01 SQLCA GLOBAL VOLATILE.\n 05 SQLCAID PIC X(8).\n 01 N PIC 9 VOLATILE VALUE 1.\n PROCEDURE DIVISION.\n GOBACK.\n");
4992 assert_eq!((p.working_storage[0].global, p.working_storage[2].picture.as_deref(), p.working_storage[2].value.is_some()), (true, Some("9"), true));
4993 }
4994
4995 #[test]
4996 fn a_scope_terminator_no_verb_is_open_for_is_discarded_at_e() {
4997 let p = program(" 01 A PIC 9.\n PROCEDURE DIVISION.\n IF A = 1 MOVE 2 TO A.\n END-IF.\n PERFORM 2 TIMES MOVE 3 TO A END-ADD END-PERFORM\n IF A = 2 END-IF\n GOBACK.\n");
4998 let messages: Vec<(Option<&str>, crate::Severity, &str)> = p.messages.iter().map(|m| (m.id, m.severity, m.message.as_str())).collect();
4999 let text = |t: &str| format!("{t}: an explicit scope terminator with no verb open for it; it was discarded");
5000 assert_eq!(messages, [(Some("IWS0104"), crate::Severity::Error, text("END-IF").as_str()), (Some("IWS0104"), crate::Severity::Error, text("END-ADD").as_str())]);
5001 let statements = format!("{:?}", p.paragraphs[0].statements);
5002 assert_eq!((p.paragraphs[0].statements.len(), statements.matches("Move").count(), statements.contains("END-ADD")), (6, 2, false), "{statements}");
5003 }
5004
5005}