1use crate::ast::*;
2use crate::lexer::{Tok, Token};
3use crate::{Error, Pos};
4
5mod declaratives;
6mod oo;
7mod report;
8mod sort;
9
10pub fn parse(tokens: &[Token], options: Vec<String>) -> Result<Vec<Program>, Error> {
14 let mut parser = Parser::new(tokens);
15 let mut programs = Vec::new();
16 parser.program(&options, &mut programs)?;
17 while parser.peek().is_some() {
18 if !parser.at_division(&["IDENTIFICATION", "ID"]) {
19 return Err(parser.error("another program, or the end of the source"));
20 }
21 parser.program(&options, &mut programs)?;
22 }
23 if let Some(first) = programs.iter().position(|p| p.function.is_none()) {
24 programs[..=first].rotate_right(1);
25 }
26 Ok(programs)
27}
28
29const VERBS: &[&str] = &[
31 "MOVE", "COMPUTE", "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "IF", "PERFORM", "DISPLAY", "INITIALIZE", "GO", "GOBACK", "STOP",
32 "CONTINUE", "EXIT", "EVALUATE", "SET", "CALL", "ACCEPT", "STRING", "UNSTRING", "INSPECT", "READ", "WRITE", "OPEN", "CLOSE",
33 "REWRITE", "DELETE", "START", "SEARCH", "SORT", "MERGE", "RETURN", "RELEASE", "CANCEL", "EXEC", "NEXT", "INVOKE",
34 "INITIATE", "GENERATE", "TERMINATE", "SUPPRESS", "ALTER", "ENTRY", "JSON", "XML",
35];
36
37fn relation(subject: Expr, op: RelOp, negated: bool, object: Expr) -> Cond {
40 let c = Cond::Rel(subject, op, object);
41 if negated { Cond::Not(Box::new(c)) } else { c }
42}
43
44fn digits(t: &Tok) -> bool {
46 matches!(t, Tok::Number(n) if n.bytes().all(|b| b.is_ascii_digit()))
47}
48
49const PHRASE_WORDS: &[&str] = &[
50 "ELSE", "END-IF", "END-PERFORM", "END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE", "END-DISPLAY", "END-ACCEPT", "WHEN",
51 "TO", "FROM", "BY", "INTO", "GIVING", "REMAINDER", "ROUNDED", "ON", "NOT", "SIZE", "UNTIL", "VARYING", "TIMES", "THRU", "THROUGH",
52 "AND", "OR", "THEN", "UPON", "WITH", "IS", "END-EVALUATE", "ALSO", "OTHER", "OF", "IN", "AT", "END", "END-READ", "END-WRITE",
53 "BEFORE", "AFTER", "ADVANCING", "INPUT", "OUTPUT", "EXTEND", "I-O", "REVERSED", "USING", "RETURNING", "EXCEPTION", "OVERFLOW",
54 "END-CALL", "OMITTED", "CONTENT", "REFERENCE", "VALUE", "UP", "DOWN", "DELIMITED", "DELIMITER", "COUNT", "POINTER", "TALLYING",
55 "REPLACING", "CONVERTING", "INITIAL", "FOR", "CHARACTERS", "LEADING", "FIRST", "ALL", "END-STRING", "END-UNSTRING", "END-SEARCH",
56 "NEXT", "INVALID", "KEY", "END-REWRITE", "END-DELETE", "END-START", "END-INVOKE", "END-RETURN", "END-OF-PAGE", "EOP", "END-JSON", "END-XML",
57];
58
59const JSON_PHRASES: &[&str] = &["COUNT", "INDICATING", "ENCODING", "NAME", "SUPPRESS", "CONVERTING", "IGNORING", "WITH", "DETAIL", "ON", "NOT", "EXCEPTION", "END-JSON", "ALSO"];
62
63const XML_PHRASES: &[&str] =
65 &["COUNT", "WITH", "ENCODING", "XML-DECLARATION", "ATTRIBUTES", "NAMESPACE", "NAMESPACE-PREFIX", "NAME", "TYPE", "SUPPRESS", "EVERY", "ON", "NOT", "EXCEPTION", "END-XML"];
66
67pub const DEVICE_ENVIRONMENT_NAMES: &[&str] = &["SYSIN", "SYSIPT", "SYSOUT", "SYSLIST", "SYSLST", "SYSPUNCH", "SYSPCH", "CONSOLE"];
70
71const ACCEPT_DEVICES: &[&str] = &["SYSIN", "SYSIPT", "CONSOLE"];
73
74const GNUCOBOL_ACCEPT_SOURCES: &[&str] = &["ENVIRONMENT-VALUE", "ESCAPE", "EXCEPTION", "LINES", "COLUMNS", "CRT", "USER"];
76
77fn advancing_environment_name(word: &str) -> bool {
80 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));
81 matches!(word, "CSP" | "AFP-5A") || numbered('C', 12) || numbered('S', 5)
82}
83
84fn upsi_switch(word: &str) -> Option<u8> {
86 match word.strip_prefix("UPSI-")?.as_bytes() {
87 [d @ b'0'..=b'7'] => Some(d - b'0'),
88 _ => None,
89 }
90}
91
92fn fixed_point(mantissa: &str, exponent: i32) -> Option<String> {
94 let (sign, body) = match mantissa.strip_prefix('-') {
95 Some(body) => ("-", body),
96 None => ("", mantissa.trim_start_matches('+')),
97 };
98 let (int, frac) = body.split_once('.')?;
99 let digits = format!("{int}{frac}");
100 let point = int.len() as i64 + i64::from(exponent);
101 let (int, frac) = match usize::try_from(point) {
102 Err(_) => (String::new(), "0".repeat(point.unsigned_abs() as usize) + &digits),
103 Ok(p) if p >= digits.len() => (digits.clone() + &"0".repeat(p - digits.len()), String::new()),
104 Ok(p) => (digits[..p].to_owned(), digits[p..].to_owned()),
105 };
106 let (int, frac) = (int.trim_start_matches('0'), frac.trim_end_matches('0'));
107 let int = if int.is_empty() { "0" } else { int };
108 (int.len() + frac.len() <= 31).then(|| if frac.is_empty() { format!("{sign}{int}") } else { format!("{sign}{int}.{frac}") })
109}
110
111fn figurative(word: &str) -> Option<Figurative> {
112 Some(match word {
113 "ZERO" | "ZEROS" | "ZEROES" => Figurative::Zero,
114 "SPACE" | "SPACES" => Figurative::Space,
115 "HIGH-VALUE" | "HIGH-VALUES" => Figurative::HighValue,
116 "LOW-VALUE" | "LOW-VALUES" => Figurative::LowValue,
117 "QUOTE" | "QUOTES" => Figurative::Quote,
118 "NULL" | "NULLS" => Figurative::Null,
119 _ => return None,
120 })
121}
122
123fn currency_symbol(c: char) -> bool {
126 u32::from(c) < 256 && !c.is_ascii_digit() && !"ABCDEGNPRSUVXZabcdegnprsuvxz +-,.*/;()\"='".contains(c)
127}
128
129fn hex_text(bytes: &[u8]) -> String {
130 format!("X'{}'", bytes.iter().map(|b| format!("{b:02X}")).collect::<String>())
131}
132
133pub fn decode_currency(environment: &mut Environment, decode: impl Fn(&[u8]) -> String) -> Result<(), crate::messages::Refused> {
136 for k in 0..environment.currency.len() {
137 let sign = &mut environment.currency[k];
138 let Some(bytes) = &sign.hex else { continue };
139 let (value, shown) = (decode(bytes), hex_text(bytes));
140 if sign.symbol == HEX_SYMBOL {
141 match value.chars().next() {
142 Some(symbol) if currency_symbol(symbol) => sign.symbol = symbol,
143 _ => return Err((crate::messages::IWS0095, format!("CURRENCY SIGN {shown} is {value:?} in the program's code page, which cannot be a PICTURE currency symbol"))),
144 }
145 } else if value.chars().any(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | '.' | ',')) {
146 return Err((crate::messages::IWS0096, format!("CURRENCY SIGN {shown} is {value:?} in the program's code page, which contains a digit, +, -, . or ,")));
147 }
148 sign.value = value;
149 sign.hex = None;
150 let symbol = sign.symbol;
151 if environment.currency.iter().filter(|c| c.symbol == symbol).count() > 1 {
152 return Err((crate::messages::IWS0036, format!("a second CURRENCY SIGN clause for the currency symbol {symbol:?}")));
153 }
154 }
155 Ok(())
156}
157
158fn share_configuration(outer: &Environment, inner: &mut Environment) {
162 inner.debugging_mode |= outer.debugging_mode;
163 inner.decimal_point_comma |= outer.decimal_point_comma;
164 if inner.currency.is_empty() {
165 inner.currency.clone_from(&outer.currency);
166 }
167 if inner.collating_sequence.is_none() {
168 inner.collating_sequence.clone_from(&outer.collating_sequence);
169 }
170 for (name, alphabet) in &outer.alphabets {
171 if !inner.alphabets.iter().any(|(n, _)| n == name) {
172 inner.alphabets.push((name.clone(), alphabet.clone()));
173 }
174 }
175}
176
177fn global_records(entries: &[DataEntry]) -> Vec<DataEntry> {
179 let mut out = Vec::new();
180 let mut taking = false;
181 for e in entries {
182 if matches!(e.level, 1 | 77) {
183 taking = e.global;
184 }
185 if taking {
186 out.push(e.clone());
187 }
188 }
189 out
190}
191
192fn function_name(name: &str) -> Result<(), &'static str> {
195 if name.len() > 30 {
196 return Err("a function name has at most 30 characters");
197 }
198 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') || !name.chars().any(|c| c.is_ascii_alphabetic()) {
199 return Err("a function name is letters, digits, hyphens and underscores, with a letter among them");
200 }
201 if name.starts_with('-') || name.ends_with('-') {
202 return Err("a function name neither starts nor ends with a hyphen");
203 }
204 if figurative(name).is_some() {
205 return Err("a figurative constant cannot name a function");
206 }
207 Ok(())
208}
209
210fn usage_word(word: &str) -> Option<Usage> {
211 Some(match word {
212 "DISPLAY" => Usage::Display,
213 "BINARY" | "COMP" | "COMPUTATIONAL" | "COMP-4" | "COMPUTATIONAL-4" => Usage::Binary,
214 "COMP-5" | "COMPUTATIONAL-5" => Usage::NativeBinary,
215 "PACKED-DECIMAL" | "COMP-3" | "COMPUTATIONAL-3" => Usage::Packed,
216 "COMP-1" | "COMPUTATIONAL-1" => Usage::Float1,
217 "COMP-2" | "COMPUTATIONAL-2" => Usage::Float2,
218 "NATIONAL" => Usage::National,
219 "DISPLAY-1" => Usage::Dbcs,
220 "POINTER" => Usage::Pointer,
221 "INDEX" => Usage::Index,
222 "FUNCTION-POINTER" | "PROCEDURE-POINTER" => Usage::ProgramPointer,
223 _ => return None,
224 })
225}
226
227struct Parser<'a> {
228 tokens: &'a [Token],
229 at: usize,
230 exec_declarations: Vec<ExecBlock>,
232 cics: bool,
234 dli: bool,
236 intrinsics: Vec<String>,
239 mnemonics: Vec<(String, String)>,
243 switches: Vec<Switch>,
246 classes: Vec<ClassClause>,
248 sql: SqlState,
249 debugging: bool,
251 messages: Vec<Error>,
253 reported: Vec<bool>,
256 functions: Vec<Prototype>,
258 defined: Vec<String>,
259 in_prototype: bool,
261}
262
263#[derive(Default)]
266struct SqlState {
267 whenever: crate::sql::Whenever,
268 blocks: u32,
269 cursors: crate::sql::Cursors,
270}
271
272type R<T> = Result<T, Error>;
273
274impl<'a> Parser<'a> {
275 fn new(tokens: &'a [Token]) -> Self {
276 Self {
277 tokens,
278 at: 0,
279 exec_declarations: Vec::new(),
280 cics: false,
281 dli: false,
282 intrinsics: Vec::new(),
283 sql: SqlState::default(),
284 mnemonics: Vec::new(),
285 switches: Vec::new(),
286 classes: Vec::new(),
287 debugging: false,
288 messages: Vec::new(),
289 reported: vec![false; tokens.len()],
290 functions: Vec::new(),
291 defined: Vec::new(),
292 in_prototype: false,
293 }
294 }
295}
296
297impl Parser<'_> {
298 fn peek(&self) -> Option<&Tok> {
299 self.tokens.get(self.at).map(|t| &t.tok)
300 }
301
302 fn peek_at(&self, ahead: usize) -> Option<&Tok> {
303 self.tokens.get(self.at + ahead).map(|t| &t.tok)
304 }
305
306 fn qualifying_paren_at(&self, at: usize) -> bool {
309 self.tokens.get(at).is_some_and(|t| t.tok == Tok::LParen && !t.after_comma)
310 }
311
312 fn pos(&self) -> Pos {
313 self.tokens.get(self.at).or(self.tokens.last()).map(|t| t.pos).unwrap_or_default()
314 }
315
316 fn word(&self) -> Option<&str> {
317 match self.peek() {
318 Some(Tok::Word(w)) => Some(w),
319 _ => None,
320 }
321 }
322
323 fn word_at(&self, ahead: usize) -> Option<&str> {
324 match self.peek_at(ahead) {
325 Some(Tok::Word(w)) => Some(w),
326 _ => None,
327 }
328 }
329
330 fn is_word(&self, w: &str) -> bool {
331 self.word() == Some(w)
332 }
333
334 fn accept_word(&mut self, w: &str) -> bool {
335 let yes = self.is_word(w);
336 if yes {
337 self.at += 1;
338 }
339 yes
340 }
341
342 fn accept_any(&mut self, words: &[&str]) -> Option<String> {
343 let w = self.word().filter(|w| words.contains(w))?.to_owned();
344 self.at += 1;
345 Some(w)
346 }
347
348 fn expect_word(&mut self, w: &str) -> R<()> {
349 if self.accept_word(w) { Ok(()) } else { Err(self.error(format!("expected {w}"))) }
350 }
351
352 fn accept(&mut self, tok: &Tok) -> bool {
353 let yes = self.peek() == Some(tok);
354 if yes {
355 self.at += 1;
356 }
357 yes
358 }
359
360 fn expect(&mut self, tok: &Tok, what: &str) -> R<()> {
361 if self.accept(tok) { Ok(()) } else { Err(self.error(format!("expected {what}"))) }
362 }
363
364 fn error(&self, message: impl Into<String>) -> Error {
365 let found = match self.peek() {
366 None => "end of source".to_owned(),
367 Some(Tok::Word(w)) => w.clone(),
368 Some(t) => format!("{t:?}"),
369 };
370 crate::messages::IWS0001.at(self.pos(), format!("{}, found {found}", message.into()))
371 }
372
373 fn name(&mut self, what: &str) -> R<String> {
374 match self.peek() {
375 Some(Tok::Word(w)) => {
376 let w = w.clone();
377 self.at += 1;
378 Ok(w)
379 }
380 _ => Err(self.error(format!("expected {what}"))),
381 }
382 }
383
384 fn at_division(&self, names: &[&str]) -> bool {
385 self.word().is_some_and(|w| names.contains(&w)) && self.word_at(1) == Some("DIVISION")
386 }
387
388 fn program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
389 let (start, first) = (self.at, out.len());
390 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);
391 let outer_messages = std::mem::take(&mut self.messages);
392 let (outer_intrinsics, outer_switches, outer_classes) = (self.intrinsics.clone(), self.switches.clone(), self.classes.clone());
393 let parsed = self.one_program(options, out);
394 (self.intrinsics, self.switches, self.classes) = (outer_intrinsics, outer_switches, outer_classes);
395 (self.exec_declarations, self.cics, self.dli, self.sql.blocks, self.mnemonics, self.debugging) = outer;
396 let own = std::mem::replace(&mut self.messages, outer_messages);
397 parsed?;
398 let mut messages = Vec::new();
399 for i in start..self.at {
400 if !std::mem::replace(&mut self.reported[i], true) {
401 messages.extend(self.tokens[i].messages.iter().cloned());
402 }
403 }
404 messages.extend(own);
405 if let Some(p) = out.get_mut(first) {
406 p.messages = messages;
407 }
408 Ok(())
409 }
410
411 fn one_program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
412 if !self.accept_word("IDENTIFICATION") {
413 self.expect_word("ID")?;
414 }
415 self.expect_word("DIVISION")?;
416 self.expect(&Tok::Period, "a period")?;
417 if self.is_word("CLASS-ID") {
418 return self.class_definition(options, out);
419 }
420 if self.is_word("FUNCTION-ID") {
421 return self.function_definition(options, out);
422 }
423 self.expect_word("PROGRAM-ID")?;
424 self.accept(&Tok::Period);
425 let id = match self.peek() {
426 Some(Tok::Alnum(s)) => {
427 let s = s.clone();
428 self.at += 1;
429 s
430 }
431 _ => self.name("a program name")?,
432 };
433 let (mut initial, mut recursive, mut common) = (false, false, false);
434 while let Some(t) = self.peek() {
435 if *t == Tok::Period {
436 self.at += 1;
437 break;
438 }
439 initial |= self.is_word("INITIAL");
440 recursive |= self.is_word("RECURSIVE");
441 common |= self.is_word("COMMON");
442 self.at += 1;
443 }
444 let first = out.len();
445 self.program_body(id, initial, recursive, options, out, false)?;
446 out[first].common = common;
447 Ok(())
448 }
449
450 fn program_body(&mut self, id: String, initial: bool, recursive: bool, options: &[String], out: &mut Vec<Program>, method: bool) -> R<()> {
453 while self.peek().is_some() && !self.at_division(&["ENVIRONMENT", "DATA", "PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() {
454 self.at += 1;
455 }
456 let (mut files, mut repository, mut environment) = (Vec::new(), Vec::new(), Environment::default());
457 if self.at_division(&["ENVIRONMENT"]) {
458 (files, repository) = self.environment(&mut environment)?;
459 }
460 self.mnemonics.splice(0..0, environment.mnemonics.iter().cloned());
461 self.switches.splice(0..0, std::mem::take(&mut environment.switches));
462 environment.switches = self.switches.clone();
463 self.classes.splice(0..0, std::mem::take(&mut environment.classes));
464 environment.classes = self.classes.clone();
465 self.debugging |= environment.debugging_mode;
466 let (mut working_storage, mut local_storage, mut linkage) = (Vec::new(), Vec::new(), Vec::new());
467 let mut report_writer = crate::report::ReportWriter::default();
468 let mut declaratives = Declaratives::default();
469 if self.at_division(&["DATA"]) {
470 self.at += 2;
471 self.expect(&Tok::Period, "a period")?;
472 while !self.at_division(&["PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() && self.peek().is_some() {
473 if self.data_exec()? {
474 continue;
475 }
476 let section = self.name("a DATA DIVISION section")?;
477 self.expect_word("SECTION")?;
478 self.expect(&Tok::Period, "a period")?;
479 match section.as_str() {
480 "WORKING-STORAGE" => working_storage = self.data_entries()?,
481 "LINKAGE" => linkage = self.data_entries()?,
482 "LOCAL-STORAGE" => local_storage = self.data_entries()?,
483 "FILE" => self.file_section(&mut files)?,
484 "REPORT" => report_writer.reports.extend(self.report_section()?),
485 other => return Err(crate::messages::IWR0006.at(self.pos(), format!("the {other} SECTION is not supported yet"))),
486 }
487 }
488 }
489 let (mut using, mut returning) = (Vec::new(), None);
490 let paragraphs = if self.at_division(&["PROCEDURE"]) {
491 self.at += 2;
492 if self.accept_word("USING") {
493 using = self.parameters()?;
494 }
495 if self.accept_word("RETURNING") {
496 returning = Some(self.name("a RETURNING item")?);
497 }
498 self.expect(&Tok::Period, "a period after the PROCEDURE DIVISION header")?;
499 self.procedure_paragraphs(&mut report_writer, &mut declaratives)?
500 } else {
501 Vec::new()
502 };
503 declaratives::debugging_sections_allowed(&declaratives, recursive, method)?;
504 if let Some(f) = files.iter().find(|f| f.assign.is_empty()) {
505 return Err(crate::messages::IWS0028.at(f.pos, format!("{} has no SELECT ... ASSIGN", f.name)));
506 }
507 if self.cics {
508 self.translator_additions(&mut linkage, &mut using)?;
509 }
510 let declares = |entries: &[DataEntry], name: &str| entries.iter().any(|e| e.name.as_deref() == Some(name));
511 if self.dli && !declares(&working_storage, "DIBSTAT") && !declares(&linkage, "DIBSTAT") {
512 working_storage.splice(0..0, system_entries("DLZDIB")?);
513 }
514 let exec_declarations = std::mem::take(&mut self.exec_declarations);
515 let (mut nested, mut contained) = (Vec::new(), Vec::new());
516 while self.at_division(&["IDENTIFICATION", "ID"]) {
517 if self.word_at(3) == Some("FUNCTION-ID") {
518 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"));
519 }
520 let first = nested.len();
521 self.program(options, &mut nested)?;
522 contained.extend(nested.get(first).map(|p: &Program| p.id.clone()));
523 }
524 let entry = nested.iter().flat_map(|p: &Program| &p.paragraphs).flat_map(|p| &p.statements).find_map(|s| match s {
525 Stmt::Entry { pos, .. } => Some(*pos),
526 _ => None,
527 });
528 if let Some(pos) = entry {
529 return Err(crate::messages::IWS0030.at(pos, "ENTRY cannot be used in a nested program"));
530 }
531 oo::share_repository(&repository, &mut nested)?;
532 let container = Container {
533 id: id.clone(),
534 working_storage: global_records(&working_storage),
535 local_storage: global_records(&local_storage),
536 linkage: global_records(&linkage),
537 files: files.iter().filter(|f| f.global).cloned().collect(),
538 };
539 for inner in &mut nested {
540 share_configuration(&environment, &mut inner.environment);
541 inner.containers.push(container.clone());
542 }
543 declaratives::contained_programs(&report_writer, &nested)?;
544 if !method && self.at_end_program() && self.word_at(1) == Some("PROGRAM") {
545 self.at += 2;
546 if self.word().is_some() || matches!(self.peek(), Some(Tok::Alnum(_))) {
547 self.at += 1;
548 }
549 self.accept(&Tok::Period);
550 }
551 out.push(Program {
552 id,
553 options: options.to_vec(),
554 initial,
555 recursive,
556 working_storage,
557 local_storage,
558 linkage,
559 using,
560 returning,
561 paragraphs,
562 files,
563 exec_declarations,
564 report_writer,
565 declaratives,
566 oo: oo::program_oo(repository),
567 environment,
568 nested: contained,
569 prototypes: self.functions.clone(),
570 ..Program::default()
571 });
572 out.extend(nested);
573 Ok(())
574 }
575
576 fn function_definition(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
579 let pos = self.pos();
580 self.at += 1;
581 self.accept(&Tok::Period);
582 let name = self.name("a function name")?;
583 function_name(&name).map_err(|why| crate::messages::IWS0031.at(self.tokens[self.at - 1].pos, format!("FUNCTION-ID {name}: {why}")))?;
584 if rt::intrinsic::FIRST.contains(&name.as_str()) || rt::intrinsic::FUNCTIONS.contains(&name.as_str()) {
585 return Err(crate::messages::IWS0032.at(pos, format!("FUNCTION-ID {name}: {name} is an intrinsic function's name (assumption C271)")));
586 }
587 let (mut external, mut prototype) = (name.clone(), false);
588 while !self.accept(&Tok::Period) {
589 if self.accept_word("AS") {
590 match self.peek() {
591 Some(Tok::Alnum(s)) if !s.trim().is_empty() => external = s.clone(),
592 _ => return Err(self.error("the function's external name, an alphanumeric literal, after AS")),
593 }
594 self.at += 1;
595 } else if self.accept_word("IS") || self.is_word("PROTOTYPE") {
596 self.expect_word("PROTOTYPE")?;
597 prototype = true;
598 } else if let Some(phrase) = self.accept_any(&["ENTRY-NAME", "ENTRY-INTERFACE"]) {
599 self.accept_word("IS");
600 let allowed: &[&str] = if phrase == "ENTRY-NAME" { &["COMPAT", "LONGUPPER", "LONGMIXED"] } else { &["STATIC", "DYNAMIC", "DLL"] };
601 if self.accept_any(allowed).is_none() {
602 return Err(self.error(format!("{} after {phrase}", allowed.join(", "))));
603 }
604 } else {
605 return Err(self.error("AS, IS PROTOTYPE, ENTRY-NAME, ENTRY-INTERFACE or the period ending the FUNCTION-ID paragraph"));
606 }
607 }
608 if !prototype && self.defined.contains(&name) {
609 return Err(crate::messages::IWS0033.at(pos, format!("a second definition of user-defined function {name}")));
610 }
611 let first = out.len();
612 self.in_prototype = prototype;
613 let body = self.program_body(name.clone(), false, true, options, out, false);
614 self.in_prototype = false;
615 body?;
616 let program = &mut out[first];
617 program.function = Some(Function { external, prototype, pos });
618 if let Some(inner) = program.nested.first() {
619 return Err(crate::messages::IWS0034.at(pos, format!("FUNCTION-ID {name}: a user-defined function contains no programs, but {inner} is inside it")));
620 }
621 if !(self.at_end_program() && self.word_at(1) == Some("FUNCTION")) {
622 return Err(self.error(format!("END FUNCTION {name}, which ends a user-defined function")));
623 }
624 self.at += 2;
625 let end = self.name("the function name after END FUNCTION")?;
626 if end != name {
627 return Err(crate::messages::IWS0035.at(self.tokens[self.at - 1].pos, format!("END FUNCTION {end} ends function {name}")));
628 }
629 self.accept(&Tok::Period);
630 let own = Prototype {
631 name,
632 external: program.load_name().to_owned(),
633 using: program.using.clone(),
634 returning: program.returning.clone(),
635 linkage: program.linkage.clone(),
636 environment: program.environment.clone(),
637 pos,
638 };
639 program.prototypes.push(own.clone());
640 self.functions.push(own);
641 if !prototype {
642 self.defined.push(program.id.clone());
643 }
644 Ok(())
645 }
646
647 fn parameters(&mut self) -> R<Vec<Param>> {
649 let (mut using, mut by_value) = (Vec::new(), false);
650 loop {
651 if self.accept_word("BY") {
652 by_value = self.accept_any(&["REFERENCE", "VALUE"]).as_deref() == Some("VALUE");
653 continue;
654 }
655 if !self.starts_ref() {
656 return Ok(using);
657 }
658 using.push(Param { by_value, name: self.name("a LINKAGE item")? });
659 }
660 }
661
662 fn at_end_program(&self) -> bool {
663 self.is_word("END") && matches!(self.word_at(1), Some("PROGRAM" | "METHOD" | "FUNCTION"))
664 }
665
666 fn environment(&mut self, clauses: &mut Environment) -> R<(Vec<FileDecl>, Vec<ClassEntry>)> {
669 let (mut files, mut repository) = (Vec::new(), Vec::new());
670 let mut special_names = false;
671 while self.peek().is_some() && !self.at_division(&["DATA", "PROCEDURE"]) {
672 if let Some(paragraph) = self.word().filter(|w| matches!(*w, "SPECIAL-NAMES" | "REPOSITORY" | "SOURCE-COMPUTER" | "OBJECT-COMPUTER" | "INPUT-OUTPUT" | "FILE-CONTROL" | "I-O-CONTROL")) {
673 special_names = paragraph == "SPECIAL-NAMES";
674 }
675 if self.accept_word("DECIMAL-POINT") {
676 self.accept_word("IS");
677 self.expect_word("COMMA")?;
678 clauses.decimal_point_comma = true;
679 continue;
680 }
681 if self.is_word("CURRENCY") {
682 let pos = self.pos();
683 self.at += 1;
684 let sign = self.currency_sign()?;
685 if sign.symbol != HEX_SYMBOL && clauses.currency.iter().any(|c| c.symbol == sign.symbol) {
686 return Err(crate::messages::IWS0036.at(pos, format!("a second CURRENCY SIGN clause for the currency symbol {:?}", sign.symbol)));
687 }
688 clauses.currency.push(sign);
689 continue;
690 }
691 if self.environment_clause(clauses)? {
692 continue;
693 }
694 if self.is_word("DEBUGGING") && self.word_at(1) == Some("MODE") {
695 self.at += 2;
696 clauses.debugging_mode = true;
697 continue;
698 }
699 if special_names && self.is_word("CLASS") && self.word_at(1).is_some() {
700 let pos = self.pos();
701 self.at += 1;
702 clauses.classes.push(self.class_clause(pos)?);
703 continue;
704 }
705 if let Some(number) = self.word().and_then(upsi_switch) {
706 let pos = self.pos();
707 self.at += 1;
708 clauses.switches.push(self.switch(number, pos)?);
709 continue;
710 }
711 if let Some(environment) = self.word().filter(|w| advancing_environment_name(w) || DEVICE_ENVIRONMENT_NAMES.contains(w)).map(str::to_owned) {
712 let at_name = if self.word_at(1) == Some("IS") { 2 } else { 1 };
713 if let Some(name) = self.word_at(at_name).map(str::to_owned) {
714 self.at += at_name + 1;
715 clauses.mnemonics.push((name, environment));
716 continue;
717 }
718 }
719 if self.accept_word("SELECT") {
720 files.push(self.select()?);
721 continue;
722 }
723 if self.is_word("REPOSITORY") && self.in_prototype {
724 return Err(self.error("no REPOSITORY paragraph: a function prototype cannot have one"));
725 }
726 if self.accept_word("REPOSITORY") {
727 repository = self.repository()?;
728 continue;
729 }
730 self.at += 1;
731 }
732 Ok((files, repository))
733 }
734
735 fn class_clause(&mut self, pos: Pos) -> R<ClassClause> {
742 let name = self.name("a class-name")?;
743 self.accept_word("IS");
744 let mut members = Vec::new();
745 while matches!(self.peek(), Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::Number(_) | Tok::National(_) | Tok::Dbcs(_))) {
746 let first = self.literal()?;
747 let last = if self.accept_any(&["THROUGH", "THRU"]).is_some() { Some(self.literal()?) } else { None };
748 members.push((first, last));
749 }
750 if members.is_empty() {
751 return Err(self.error(format!("a literal after CLASS {name}")));
752 }
753 Ok(ClassClause { name, members, pos })
754 }
755
756 fn switch(&mut self, number: u8, pos: Pos) -> R<Switch> {
757 let status = |w: &str| matches!(w, "ON" | "OFF");
758 let mnemonic = if self.accept_word("IS") || self.word().is_some_and(|w| !status(w) && !rt::reserved_words::is_reserved(w)) {
759 Some(self.name("a mnemonic-name")?)
760 } else {
761 None
762 };
763 let (mut on, mut off) = (None, None);
764 while let Some(which) = self.accept_any(&["ON", "OFF"]) {
765 self.accept_word("STATUS");
766 self.accept_word("IS");
767 let name = self.name("a condition-name")?;
768 let slot = if which == "ON" { &mut on } else { &mut off };
769 if slot.replace(name).is_some() {
770 return Err(crate::messages::IWS0037.at(pos, format!("UPSI-{number}: a second {which} STATUS phrase")));
771 }
772 }
773 if mnemonic.is_none() && on.is_none() && off.is_none() {
774 return Err(crate::messages::IWS0038.at(pos, format!("UPSI-{number}: a mnemonic-name or an ON or OFF STATUS phrase must follow it")));
775 }
776 Ok(Switch { number, mnemonic, on, off, pos })
777 }
778
779 fn currency_sign(&mut self) -> R<CurrencySign> {
782 self.accept_word("SIGN");
783 self.accept_word("IS");
784 let pos = self.pos();
785 let (value, hex) = match self.literal()? {
786 Literal::Alnum(v) if !v.is_empty() => (v, None),
787 Literal::Hex(b) if !b.is_empty() => (String::new(), Some(b)),
788 _ => return Err(crate::messages::IWS0039.at(pos, "CURRENCY SIGN needs a nonempty alphanumeric literal")),
789 };
790 let with = self.accept_word("WITH");
791 if !(self.is_word("PICTURE") && self.word_at(1) == Some("SYMBOL")) {
792 if with {
793 return Err(self.error("expected PICTURE SYMBOL after WITH"));
794 }
795 if let Some(bytes) = hex {
796 return match bytes.len() {
797 1 => Ok(CurrencySign { value, symbol: HEX_SYMBOL, hex: Some(bytes) }),
798 _ => Err(crate::messages::IWS0040.at(pos, format!("CURRENCY SIGN {} is not one character that can be a PICTURE currency symbol", hex_text(&bytes)))),
799 };
800 }
801 let mut chars = value.chars();
802 return match (chars.next(), chars.next()) {
803 (Some(symbol), None) if currency_symbol(symbol) => Ok(CurrencySign { value, symbol, hex }),
804 _ => Err(crate::messages::IWS0041.at(pos, format!("CURRENCY SIGN {value:?} is not one character that can be a PICTURE currency symbol"))),
805 };
806 }
807 self.at += 2;
808 if value.chars().any(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | '.' | ',')) {
809 return Err(crate::messages::IWS0042.at(pos, format!("CURRENCY SIGN {value:?} contains a digit, +, -, . or ,")));
810 }
811 let pos = self.pos();
812 let symbol = match self.literal()? {
813 Literal::Alnum(s) => s,
814 _ => String::new(),
815 };
816 let mut chars = symbol.chars();
817 match (chars.next(), chars.next()) {
818 (Some(symbol), None) if currency_symbol(symbol) => Ok(CurrencySign { value, symbol, hex }),
819 _ => Err(crate::messages::IWS0043.at(pos, format!("PICTURE SYMBOL {symbol:?} is not one character that can be a PICTURE currency symbol"))),
820 }
821 }
822
823 fn select(&mut self) -> R<FileDecl> {
824 let pos = self.pos();
825 let optional = self.accept_word("OPTIONAL");
826 let name = self.name("a file name")?;
827 let mut f = FileDecl {
828 name,
829 assign: String::new(),
830 assign_item: None,
831 organization: Organization::Sequential,
832 access: Access::Sequential,
833 record_key: None,
834 alternate_keys: Vec::new(),
835 relative_key: None,
836 optional,
837 status: None,
838 vsam_status: None,
839 passwords: Vec::new(),
840 recording: None,
841 record_min: None,
842 record_max: None,
843 record_varying: false,
844 record_depending: None,
845 records: Vec::new(),
846 reports: Vec::new(),
847 linage: None,
848 sort: false,
849 external: false,
850 global: false,
851 declared_in: None,
852 pos,
853 };
854 let mut delimiter = None;
855 while !self.accept(&Tok::Period) {
856 let clause = self.name("a SELECT clause or a period")?;
857 match clause.as_str() {
858 "ASSIGN" => {
859 let using = self.accept_word("USING");
860 if !using {
861 self.accept_word("TO");
862 }
863 if using || self.accept_word("DYNAMIC") {
864 let reference = self.reference()?;
865 f.assign = reference.name.clone();
866 f.assign_item = Some(AssignItem { reference, explicit: true });
867 continue;
868 }
869 let external = self.is_word("EXTERNAL") && matches!(self.tokens.get(self.at + 1).map(|t| &t.tok), Some(Tok::Word(_) | Tok::Alnum(_)));
870 if external {
871 self.at += 1;
872 }
873 let at = self.pos();
874 let (target, word) = match self.peek().cloned() {
875 Some(Tok::Word(w)) => (w, !external),
876 Some(Tok::Alnum(w)) => (w, false),
877 _ => return Err(self.error("a DD name after ASSIGN")),
878 };
879 self.at += 1;
880 if word {
881 f.assign_item = Some(AssignItem { reference: Ref { name: target.clone(), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos: at }, explicit: false });
882 }
883 let target = target.to_ascii_uppercase();
884 f.assign = target.rsplit('-').next().filter(|_| target.contains("-S-") || target.starts_with("S-") || target.starts_with("AS-")).unwrap_or(&target).to_owned();
885 while match self.peek() {
887 Some(Tok::Alnum(_)) => true,
888 Some(Tok::Word(w)) => !SELECT_CLAUSES.contains(&w.as_str()),
889 _ => false,
890 } {
891 self.at += 1;
892 }
893 }
894 "PASSWORD" => {
895 self.accept_word("IS");
896 f.passwords.push(self.reference()?);
897 }
898 "RECORD" if self.is_word("DELIMITER") => {
900 delimiter = Some(self.pos());
901 self.at += 1;
902 self.accept_word("IS");
903 self.name("STANDARD-1 or an assignment-name")?;
904 }
905 "RECORD" if !self.is_word("SEQUENTIAL") => {
906 self.accept_word("KEY");
907 self.accept_word("IS");
908 f.record_key = Some(self.reference()?);
909 }
910 "RELATIVE" if self.is_word("KEY") || self.is_word("IS") || self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) => {
911 self.accept_word("KEY");
912 self.accept_word("IS");
913 f.relative_key = Some(self.reference()?);
914 }
915 "ALTERNATE" => {
916 self.accept_word("RECORD");
917 self.accept_word("KEY");
918 self.accept_word("IS");
919 let key = self.reference()?;
920 let duplicates = self.accept_word("WITH") | self.is_word("DUPLICATES");
921 if duplicates {
922 self.expect_word("DUPLICATES")?;
923 }
924 f.alternate_keys.push((key, duplicates));
925 }
926 "ORGANIZATION" | "LINE" | "RECORD" | "SEQUENTIAL" | "INDEXED" | "RELATIVE" => {
927 if clause == "ORGANIZATION" {
928 self.accept_word("IS");
929 }
930 let first = if clause == "ORGANIZATION" { self.name("an organization")? } else { clause.clone() };
931 f.organization = match first.as_str() {
932 "LINE" => {
933 self.expect_word("SEQUENTIAL")?;
934 Organization::LineSequential
935 }
936 "RECORD" => {
937 self.expect_word("SEQUENTIAL")?;
938 Organization::Sequential
939 }
940 "SEQUENTIAL" => Organization::Sequential,
941 "INDEXED" => Organization::Indexed,
942 "RELATIVE" => Organization::Relative,
943 other => return Err(crate::messages::IWR0007.at(self.pos(), format!("ORGANIZATION {other} is not supported yet"))),
944 };
945 }
946 "ACCESS" => {
947 self.accept_word("MODE");
948 self.accept_word("IS");
949 f.access = match self.name("an access mode")?.as_str() {
950 "SEQUENTIAL" => Access::Sequential,
951 "RANDOM" => Access::Random,
952 "DYNAMIC" => Access::Dynamic,
953 other => return Err(self.error(format!("ACCESS MODE {other} is not an access mode"))),
954 };
955 }
956 "FILE" | "STATUS" => {
957 if clause == "FILE" {
958 self.expect_word("STATUS")?;
959 }
960 self.accept_word("IS");
961 f.status = Some(self.reference()?);
962 if self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
963 f.vsam_status = Some(self.reference()?);
964 }
965 }
966 "RESERVE" | "PADDING" => {
967 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
968 self.at += 1;
969 }
970 }
971 other => return Err(crate::messages::IWR0008.at(self.pos(), format!("{other} is not a SELECT clause ironwork for COBOL supports yet"))),
972 }
973 }
974 if let Some(at) = delimiter.filter(|_| f.organization != Organization::Sequential) {
975 return Err(crate::messages::IWS0044.at(at, format!("RECORD DELIMITER on {}: the clause is for a file of ORGANIZATION SEQUENTIAL", f.name)));
976 }
977 Ok(f)
978 }
979
980 fn file_section(&mut self, files: &mut [FileDecl]) -> R<()> {
983 while self.is_word("FD") || self.is_word("SD") {
984 let indicator = self.name("FD or SD")?;
985 let pos = self.pos();
986 let name = self.name("a file name")?;
987 let Some(index) = files.iter().position(|f| f.name == name) else {
988 return Err(crate::messages::IWS0045.at(pos, format!("{indicator} {name} has no SELECT")));
989 };
990 files[index].sort = indicator == "SD";
991 while !self.accept(&Tok::Period) {
992 match self.name("an FD clause or a period")?.as_str() {
993 "RECORDING" => {
994 self.accept_word("MODE");
995 self.accept_word("IS");
996 let mode = self.name("F, V, U or S")?;
997 files[index].recording = mode.chars().next();
998 }
999 "RECORD" => {
1000 self.accept_word("CONTAINS");
1001 self.accept_word("IS");
1002 let varying = self.accept_word("VARYING");
1003 if varying {
1004 files[index].recording.get_or_insert('V');
1005 self.accept_word("IN");
1006 self.accept_word("SIZE");
1007 self.accept_word("FROM");
1008 }
1009 let number = |p: &mut Self| -> R<Option<u32>> {
1010 match p.peek() {
1011 Some(Tok::Number(n)) => {
1012 let v = n.parse().map_err(|_| p.error("a record length"))?;
1013 p.at += 1;
1014 Ok(Some(v))
1015 }
1016 _ => Ok(None),
1017 }
1018 };
1019 let first = number(self)?;
1020 let second = if self.accept_word("TO") { number(self)? } else { None };
1021 if varying {
1022 files[index].record_varying = true;
1023 (files[index].record_min, files[index].record_max) = (first, second);
1024 } else if first.is_some() {
1025 files[index].record_min = first;
1026 files[index].record_max = second.or(first);
1027 }
1028 self.accept_word("CHARACTERS");
1029 if self.accept_word("DEPENDING") {
1030 self.accept_word("ON");
1031 files[index].record_depending = Some(self.reference()?);
1032 }
1033 }
1034 "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"))),
1035 "REPORT" | "REPORTS" => {
1036 let names = self.report_names()?;
1037 files[index].reports.extend(names);
1038 }
1039 "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"))),
1040 "EXTERNAL" => files[index].external = true,
1041 "GLOBAL" => files[index].global = true,
1042 "LINAGE" => {
1043 let linage = self.linage()?;
1044 if files[index].linage.is_some() {
1045 return Err(crate::messages::IWS0048.at(pos, format!("{indicator} {name}: LINAGE is given twice")));
1046 }
1047 files[index].linage = (indicator == "FD").then_some(linage);
1048 }
1049 _ => {
1050 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| FD_WORDS.contains(&w)) {
1051 self.at += 1;
1052 }
1053 }
1054 }
1055 }
1056 let mut records = self.data_entries()?;
1057 if let Some(e) = records.iter().find(|e| e.external) {
1058 return Err(crate::messages::IWS0049.at(e.pos, format!("{indicator} {name}: EXTERNAL goes on the FD, not on a record of the FILE SECTION")));
1059 }
1060 let (external, global) = (files[index].external, files[index].global);
1061 for e in &mut records {
1062 e.external = external;
1063 e.global |= global;
1064 }
1065 files[index].records = records;
1066 }
1067 Ok(())
1068 }
1069
1070 fn linage(&mut self) -> R<Linage> {
1072 self.accept_word("IS");
1073 let lines = self.linage_value("LINAGE")?;
1074 self.accept_word("LINES");
1075 let mut linage = Linage { lines, footing: None, top: None, bottom: None };
1076 loop {
1077 let start = self.at;
1078 self.accept_word("WITH");
1079 self.accept_word("LINES");
1080 self.accept_word("AT");
1081 let Some(phrase) = self.accept_any(&["FOOTING", "TOP", "BOTTOM"]) else {
1082 self.at = start;
1083 return Ok(linage);
1084 };
1085 if phrase == "FOOTING" {
1086 self.accept_word("AT");
1087 }
1088 let value = Some(self.linage_value(&phrase)?);
1089 let slot = match phrase.as_str() {
1090 "FOOTING" => &mut linage.footing,
1091 "TOP" => &mut linage.top,
1092 _ => &mut linage.bottom,
1093 };
1094 if slot.is_some() {
1095 return Err(self.error(format!("LINAGE: {phrase} is given twice")));
1096 }
1097 *slot = value;
1098 }
1099 }
1100
1101 fn linage_value(&mut self, phrase: &str) -> R<LinageValue> {
1103 match self.peek() {
1104 Some(Tok::Number(n)) if n.bytes().all(|b| b.is_ascii_digit()) => {
1105 let n = n.clone();
1106 self.at += 1;
1107 Ok(LinageValue::Integer(n))
1108 }
1109 Some(Tok::Number(n)) => Err(self.error(format!("LINAGE: {phrase} {n} is not an unsigned integer"))),
1110 Some(Tok::Word(_)) if self.starts_ref() => {
1111 let r = self.reference()?;
1112 if !r.subscripts.is_empty() || r.refmod.is_some() {
1113 return Err(crate::messages::IWS0050.at(r.pos, format!("LINAGE: {phrase} {} takes no subscript or reference modification", r.name)));
1114 }
1115 Ok(LinageValue::Data(r))
1116 }
1117 _ => Err(self.error(format!("an integer or a data-name after {phrase}"))),
1118 }
1119 }
1120
1121 fn data_exec(&mut self) -> R<bool> {
1123 let Some(Tok::Exec(text)) = self.peek().cloned() else { return Ok(false) };
1124 let block = self.exec_block(&text, self.pos());
1125 self.at += 1;
1126 self.accept(&Tok::Period);
1127 self.exec_declarations.push(block);
1128 Ok(true)
1129 }
1130
1131 fn translator_additions(&mut self, linkage: &mut Vec<DataEntry>, using: &mut Vec<Param>) -> R<()> {
1134 let defined = |l: &[DataEntry], n: &str| l.iter().any(|e| e.level == 1 && e.name.as_deref() == Some(n));
1135 let mut added = Vec::new();
1136 if !defined(linkage, "DFHEIBLK") {
1137 added.extend(system_entries("DFHEIBLK")?);
1138 }
1139 if !defined(linkage, "DFHCOMMAREA") {
1140 added.extend(system_text_entries(" 01 DFHCOMMAREA PIC X(1).\n")?);
1141 }
1142 linkage.splice(0..0, added);
1143 for (i, name) in ["DFHEIBLK", "DFHCOMMAREA"].into_iter().enumerate() {
1144 if using.get(i).map(|p| p.name.as_str()) != Some(name) {
1145 using.insert(i, Param { by_value: false, name: name.into() });
1146 }
1147 }
1148 Ok(())
1149 }
1150
1151 fn exec_block(&mut self, text: &str, pos: Pos) -> ExecBlock {
1152 let (kind_word, body) = text.split_once(' ').unwrap_or((text, ""));
1153 let kind = match kind_word.to_ascii_uppercase().as_str() {
1154 "SQL" => ExecKind::Sql,
1155 "CICS" => ExecKind::Cics,
1156 "DLI" => ExecKind::Dli,
1157 _ => ExecKind::Other,
1158 };
1159 let words: Vec<String> = body.split_whitespace().map(|w| w.to_ascii_uppercase()).collect();
1160 let word = |i: usize| words.get(i).map(String::as_str).unwrap_or("");
1161 let mut block = ExecBlock { kind, command: word(0).to_owned(), options: Vec::new(), host_variables: Vec::new(), sql: None, text: text.to_owned(), pos };
1162 match kind {
1163 ExecKind::Sql => {
1164 block.command = match (word(0), word(1), word(2)) {
1165 ("DECLARE", _, "CURSOR") => "DECLARE CURSOR".into(),
1166 ("DECLARE", _, "TABLE") => "DECLARE TABLE".into(),
1167 ("DECLARE", _, "STATEMENT") => "DECLARE STATEMENT".into(),
1168 ("BEGIN" | "END", "DECLARE", "SECTION") => format!("{} DECLARE SECTION", word(0)),
1169 (first, _, _) => first.into(),
1170 };
1171 let statement = self.sql.cursors.resolve(crate::sql::parse(body, pos));
1172 if let crate::sql::Statement::Whenever { condition, action } = &statement {
1173 self.sql.whenever.set(*condition, action.clone());
1174 }
1175 block.host_variables = match &statement {
1176 crate::sql::Statement::Unsupported(_)
1177 | crate::sql::Statement::Connect { .. }
1178 | crate::sql::Statement::Malformed(_)
1179 | crate::sql::Statement::Declaration
1180 | crate::sql::Statement::DeclareUnsupported { .. } => host_variables(body, pos),
1181 typed => typed.references().into_iter().cloned().collect(),
1182 };
1183 self.sql.blocks += 1;
1184 block.sql = Some(crate::sql::Sql { statement, ordinal: self.sql.blocks, whenever: self.sql.whenever.clone() });
1185 }
1186 ExecKind::Cics => {
1187 self.cics = true;
1188 block.options = cics_options(body);
1189 if let Some((first, None)) = block.options.first().cloned() {
1190 block.command = first.clone();
1191 block.options.remove(0);
1192 if let Some((second, None)) = block.options.first().cloned()
1193 && crate::system::cics_two_word(&first, &second)
1194 {
1195 block.command = format!("{first} {second}");
1196 block.options.remove(0);
1197 }
1198 }
1199 let labels = block.command.starts_with("HANDLE");
1200 for (name, arg) in &mut block.options {
1201 if let Some(ExecArg::Text(t)) = arg
1202 && (!labels || matches!(name.as_str(), "RESP" | "RESP2" | "PROGRAM"))
1203 && let Some(op) = operand_of(t, pos)
1204 {
1205 *arg = Some(ExecArg::Operand(op));
1206 }
1207 }
1208 }
1209 ExecKind::Dli => {
1210 self.dli = true;
1211 let words: Vec<&str> = body.split_whitespace().collect();
1212 let rest = match crate::dli::command(&words) {
1213 Some((command, n)) => {
1214 block.command = command.name.to_owned();
1215 let mut rest = body.trim_start();
1216 for _ in 0..n {
1217 rest = rest.trim_start().split_once(char::is_whitespace).map_or("", |(_, r)| r);
1218 }
1219 rest
1220 }
1221 None => body.trim_start().split_once(char::is_whitespace).map_or("", |(_, r)| r),
1222 };
1223 block.options = cics_options(rest).into_iter().filter(|(name, arg)| !(name == "USING" && arg.is_none())).collect();
1224 for (name, arg) in &mut block.options {
1225 let Some(ExecArg::Text(t)) = arg else { continue };
1226 if name == "WHERE" {
1227 if let Ok(comparisons) = crate::dli::qualification(t) {
1228 for (_, _, value) in comparisons {
1229 if let Some(Operand::Ref(r)) = operand_of(&value, pos) {
1230 block.host_variables.push(r);
1231 }
1232 }
1233 }
1234 continue;
1235 }
1236 let area = t.strip_prefix('(').and_then(|a| a.strip_suffix(')'));
1237 let data = match area {
1238 Some(area) if crate::dli::NAMED.contains(&name.as_str()) => area.trim(),
1239 _ if crate::dli::NAMED.contains(&name.as_str()) => continue,
1240 _ => t.as_str(),
1241 };
1242 if let Some(op) = operand_of(data, pos) {
1243 *arg = Some(ExecArg::Operand(op));
1244 }
1245 }
1246 }
1247 _ => {}
1248 }
1249 block
1250 }
1251
1252 fn data_entries(&mut self) -> R<Vec<DataEntry>> {
1253 let mut entries = Vec::new();
1254 loop {
1255 if self.data_exec()? {
1256 continue;
1257 }
1258 let Some((level, pos)) = self.level_number()? else { break };
1259 entries.push(self.data_entry(level, pos)?);
1260 }
1261 Ok(entries)
1262 }
1263
1264 fn level_number(&mut self) -> R<Option<(u8, Pos)>> {
1265 let Some(Tok::Number(level)) = self.peek() else { return Ok(None) };
1266 let pos = self.pos();
1267 let level = level.parse().map_err(|_| self.error("a level number"))?;
1268 self.at += 1;
1269 Ok(Some((level, pos)))
1270 }
1271
1272 fn picture(&mut self) -> R<String> {
1274 self.accept_word("IS");
1275 match self.peek() {
1276 Some(Tok::Pic(p)) => {
1277 let p = p.clone();
1278 self.at += 1;
1279 Ok(p)
1280 }
1281 _ => Err(self.error("a PICTURE character-string")),
1282 }
1283 }
1284
1285 fn sign_clause(&mut self, first: &str) -> R<SignClause> {
1287 let side = if first == "SIGN" {
1288 self.accept_word("IS");
1289 self.name("LEADING or TRAILING")?
1290 } else {
1291 first.to_owned()
1292 };
1293 let position = match side.as_str() {
1294 "LEADING" => SignPosition::Leading,
1295 "TRAILING" => SignPosition::Trailing,
1296 _ => return Err(self.error("LEADING or TRAILING")),
1297 };
1298 let separate = self.accept_word("SEPARATE");
1299 if separate {
1300 self.accept_word("CHARACTER");
1301 }
1302 Ok(SignClause { position, separate })
1303 }
1304
1305 fn blank_when_zero(&mut self) -> R<()> {
1307 self.accept_word("WHEN");
1308 if self.accept_any(&["ZERO", "ZEROS", "ZEROES"]).is_none() {
1309 return Err(self.error("ZERO after BLANK WHEN"));
1310 }
1311 Ok(())
1312 }
1313
1314 fn data_entry(&mut self, level: u8, pos: Pos) -> R<DataEntry> {
1315 let mut e = DataEntry {
1316 level,
1317 name: None,
1318 spelled: None,
1319 picture: None,
1320 usage: None,
1321 value: None,
1322 redefines: None,
1323 occurs: None,
1324 occurs_min: None,
1325 depending_on: None,
1326 sign: None,
1327 justified: false,
1328 sync: false,
1329 blank_when_zero: false,
1330 indexed_by: Vec::new(),
1331 keys: Vec::new(),
1332 condition_values: Vec::new(),
1333 false_value: None,
1334 renames: None,
1335 object_class: None,
1336 external: false,
1337 global: false,
1338 pos,
1339 };
1340 if let Some(w) = self.word()
1341 && !is_clause_word(w)
1342 {
1343 if w != "FILLER" {
1344 e.name = Some(w.to_owned());
1345 e.spelled = self.tokens[self.at].spelled.clone();
1346 }
1347 self.at += 1;
1348 }
1349 let mut floating = None;
1350 while !self.accept(&Tok::Period) {
1351 let clause = self.name("a data description clause or a period")?;
1352 match clause.as_str() {
1353 "PIC" | "PICTURE" => e.picture = Some(self.picture()?),
1354 "USAGE" => {
1355 self.accept_word("IS");
1356 let w = self.name("a usage")?;
1357 if w == "OBJECT" {
1358 self.object_reference(&mut e)?;
1359 } else {
1360 e.usage = Some(usage_word(&w).ok_or_else(|| crate::messages::IWR0004.at(pos, format!("USAGE {w} is not supported yet")))?);
1361 }
1362 }
1363 "OBJECT" => self.object_reference(&mut e)?,
1364 "VALUE" | "VALUES" => {
1365 self.accept_word("IS");
1366 self.accept_word("ARE");
1367 if level == 88 {
1368 while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.is_word("WHEN") && !self.is_word("FALSE") {
1369 let low = self.literal()?;
1370 let high = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.literal()?) } else { None };
1371 e.condition_values.push((low, high));
1372 }
1373 if self.accept_word("WHEN") {
1374 self.accept_word("SET");
1375 self.accept_word("TO");
1376 self.expect_word("FALSE")?;
1377 self.accept_word("IS");
1378 e.false_value = Some(self.literal()?);
1379 } else if self.accept_word("FALSE") {
1380 self.accept_word("IS");
1381 e.false_value = Some(self.literal()?);
1382 }
1383 } else {
1384 let at = self.pos();
1385 e.value = Some(self.literal()?);
1386 if let Some(Literal::Number(mantissa)) = &e.value
1387 && let Some(fixed) = self.floating_point(&mantissa.clone(), at)?
1388 {
1389 e.value = Some(Literal::Number(fixed));
1390 floating = Some(at);
1391 }
1392 }
1393 }
1394 "REDEFINES" => e.redefines = Some(self.name("the item redefined")?),
1395 "RENAMES" => {
1396 let first = self.reference()?;
1397 let last = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.reference()?) } else { None };
1398 e.renames = Some((first, last));
1399 }
1400 "OCCURS" => {
1401 let count = |p: &mut Self| -> R<u32> {
1402 let n = match p.peek() {
1403 Some(Tok::Number(n)) => n.parse().map_err(|_| p.error("an OCCURS count"))?,
1404 _ => return Err(p.error("an OCCURS count")),
1405 };
1406 p.at += 1;
1407 Ok(n)
1408 };
1409 let mut most = count(self)?;
1410 let mut least = 1;
1411 if self.accept_word("TO") {
1412 least = most;
1413 most = count(self)?;
1414 }
1415 e.occurs = Some(most);
1416 self.accept_word("TIMES");
1417 if self.accept_word("DEPENDING") {
1418 self.accept_word("ON");
1419 e.depending_on = Some(self.reference()?);
1420 e.occurs_min = Some(least);
1421 }
1422 loop {
1423 if let Some(order) = self.accept_any(&["ASCENDING", "DESCENDING"]) {
1424 self.accept_word("KEY");
1425 self.accept_word("IS");
1426 while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "INDEXED" | "ASCENDING" | "DESCENDING")) {
1427 e.keys.push((order == "ASCENDING", self.reference()?));
1428 }
1429 } else if self.accept_word("INDEXED") {
1430 self.accept_word("BY");
1431 while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "ASCENDING" | "DESCENDING")) {
1432 e.indexed_by.push(self.name("an index name")?);
1433 }
1434 } else {
1435 break;
1436 }
1437 }
1438 }
1439 "SIGN" | "LEADING" | "TRAILING" => e.sign = Some(self.sign_clause(&clause)?),
1440 "JUSTIFIED" | "JUST" => {
1441 self.accept_word("RIGHT");
1442 e.justified = true;
1443 }
1444 "SYNC" | "SYNCHRONIZED" => {
1445 self.accept_any(&["LEFT", "RIGHT"]);
1446 e.sync = true;
1447 }
1448 "BLANK" => {
1449 self.blank_when_zero()?;
1450 e.blank_when_zero = true;
1451 }
1452 "EXTERNAL" => e.external = true,
1453 "GLOBAL" => e.global = true,
1454 "IS" if matches!(self.word(), Some("EXTERNAL" | "GLOBAL")) => {}
1455 other => match usage_word(other) {
1456 Some(u) => e.usage = Some(u),
1457 None => return Err(crate::messages::IWR0003.at(self.tokens[self.at - 1].pos, format!("{other} is not a data description clause ironwork for COBOL supports yet"))),
1458 },
1459 }
1460 }
1461 if let Some(at) = floating
1462 && (e.picture.is_some() || e.usage.is_some_and(|u| !matches!(u, Usage::Float1 | Usage::Float2)))
1463 {
1464 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"));
1465 }
1466 Ok(e)
1467 }
1468
1469 fn floating_point(&mut self, mantissa: &str, at: Pos) -> R<Option<String>> {
1473 let end = at.col + mantissa.chars().count() as u32;
1474 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);
1475 let Some(Tok::Word(word)) = self.peek().cloned() else { return Ok(None) };
1476 if !word.starts_with('E') || !mantissa.contains('.') || !adjacent(self, 0, end) {
1477 return Ok(None);
1478 }
1479 let (exponent, used) = match (word.as_str(), self.peek_at(1)) {
1480 ("E", Some(Tok::Number(n))) if n.starts_with(['+', '-']) && adjacent(self, 1, end + 1) => (n.clone(), 2),
1481 ("E", _) => return Ok(None),
1482 (_, _) => (word[1..].to_owned(), 1),
1483 };
1484 let digits = exponent.trim_start_matches(['+', '-']);
1485 if digits.is_empty() || digits.len() > 2 || exponent.len() > digits.len() + 1 || !digits.bytes().all(|b| b.is_ascii_digit()) {
1486 return Ok(None);
1487 }
1488 self.at += used;
1489 let written = format!("{mantissa}E{exponent}");
1490 if mantissa.bytes().filter(u8::is_ascii_digit).count() > 16 {
1491 return Err(crate::messages::IWS0052.at(at, format!("{written}: a floating-point literal's mantissa has at most 16 digits")));
1492 }
1493 let exponent: i32 = exponent.parse().map_err(|_| crate::messages::IWS0053.at(at, format!("{written}: not an exponent")))?;
1494 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")))
1495 }
1496
1497 fn literal(&mut self) -> R<Literal> {
1498 let lit = match self.peek().cloned() {
1499 Some(Tok::Alnum(s)) => Literal::Alnum(s),
1500 Some(Tok::Hex(b)) => Literal::Hex(b),
1501 Some(Tok::National(s)) => Literal::National(s),
1502 Some(Tok::Dbcs(s)) => Literal::Dbcs(s),
1503 Some(Tok::Number(n)) => Literal::Number(n),
1504 Some(Tok::Word(w)) if w == "ALL" => {
1505 let at = self.pos();
1506 self.at += 1;
1507 let inner = self.literal()?;
1508 let written = match &inner {
1510 Literal::Number(n) => Some(format!("ALL {n}")),
1511 Literal::All(_) => Some("ALL ALL".to_owned()),
1512 _ => None,
1513 };
1514 if let Some(written) = written {
1515 self.messages.push(crate::messages::IWS0054.at(at, format!("{written}: the literal after ALL is alphanumeric, national or a figurative constant other than ALL")));
1516 }
1517 return Ok(Literal::All(Box::new(inner)));
1518 }
1519 Some(Tok::Word(w)) => Literal::Figurative(figurative(&w).ok_or_else(|| self.error("a literal"))?),
1520 _ => return Err(self.error("a literal")),
1521 };
1522 self.at += 1;
1523 Ok(lit)
1524 }
1525
1526 fn paragraph_header(&self) -> bool {
1527 self.tokens.get(self.at).is_some_and(|t| t.area_a && (matches!(t.tok, Tok::Word(_)) || digits(&t.tok))) && self.peek_at(1) == Some(&Tok::Period)
1528 }
1529
1530 fn section_header(&self) -> bool {
1531 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")
1533 }
1534
1535 fn paragraphs(&mut self) -> R<Vec<Paragraph>> {
1536 let mut paragraphs = Vec::new();
1537 while self.peek().is_some() && !self.at_end_program() && !self.at_division(&["IDENTIFICATION", "ID"]) {
1538 if self.is_word("DECLARATIVES") {
1539 return Err(self.error("DECLARATIVES must begin the PROCEDURE DIVISION"));
1540 }
1541 self.procedure_item(&mut paragraphs)?;
1542 }
1543 Ok(paragraphs)
1544 }
1545
1546 fn procedure_item(&mut self, paragraphs: &mut Vec<Paragraph>) -> R<bool> {
1548 if self.section_header() {
1549 let pos = self.pos();
1550 let name = self.procedure_word("a section name")?;
1551 self.at += 1;
1552 let mut priority = 0;
1553 if let Some(Tok::Number(n)) = self.peek() {
1554 priority = n.trim_start_matches('+').parse().ok().filter(|p| *p <= 99).ok_or_else(|| self.error("a priority-number from 0 to 99"))?;
1555 self.at += 1;
1556 }
1557 self.expect(&Tok::Period, "a period after the section header")?;
1558 paragraphs.push(Paragraph { section: Some(name.clone()), name, statements: Vec::new(), is_section: true, priority, pos });
1559 return Ok(true);
1560 }
1561 if self.paragraph_header() {
1562 let pos = self.pos();
1563 let name = self.procedure_word("a paragraph name")?;
1564 self.at += 1;
1565 let (section, priority) = paragraphs.last().map_or((None, 0), |p| (p.section.clone(), p.priority));
1566 paragraphs.push(Paragraph { name, statements: Vec::new(), section, is_section: false, priority, pos });
1567 return Ok(false);
1568 }
1569 if self.accept(&Tok::Period) {
1570 if let Some(p) = paragraphs.last_mut()
1571 && p.statements.last().is_some_and(|s| *s != Stmt::SentenceEnd)
1572 {
1573 p.statements.push(Stmt::SentenceEnd);
1574 }
1575 return Ok(false);
1576 }
1577 let block = self.block(&[])?;
1578 if block.is_empty() {
1579 return Err(self.error("a statement"));
1580 }
1581 if paragraphs.is_empty() {
1582 paragraphs.push(Paragraph { name: String::new(), statements: Vec::new(), section: None, is_section: false, priority: 0, pos: self.pos() });
1583 }
1584 paragraphs.last_mut().unwrap().statements.extend(block);
1585 Ok(false)
1586 }
1587
1588 fn block(&mut self, stops: &[&str]) -> R<Vec<Stmt>> {
1590 let mut out = Vec::new();
1591 while let Some(tok) = self.peek() {
1592 if *tok == Tok::Period || self.paragraph_header() || self.section_header() || self.word().is_some_and(|w| stops.contains(&w)) {
1593 break;
1594 }
1595 if let Some(Tok::Exec(text)) = self.peek().cloned() {
1596 let block = self.exec_block(&text, self.pos());
1597 self.at += 1;
1598 out.push(Stmt::Exec(Box::new(block)));
1599 continue;
1600 }
1601 if !self.word().is_some_and(|w| VERBS.contains(&w)) {
1602 break;
1603 }
1604 out.push(self.statement()?);
1605 }
1606 Ok(out)
1607 }
1608
1609 fn statement(&mut self) -> R<Stmt> {
1610 let pos = self.pos();
1611 let verb = self.name("a statement")?;
1612 Ok(match verb.as_str() {
1613 "MOVE" => {
1614 if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() {
1615 return self.corresponding(CorrespondingVerb::Move, pos);
1616 }
1617 let from = self.operand()?;
1618 self.expect_word("TO")?;
1619 let to = self.refs()?;
1620 Stmt::Move { from, to, pos }
1621 }
1622 "COMPUTE" => {
1623 let targets = self.targets()?;
1624 if !self.accept(&Tok::Eq) {
1625 self.expect_word("EQUAL")?;
1626 }
1627 let expr = self.expr()?;
1628 let size_error = self.size_error()?;
1629 self.accept_word("END-COMPUTE");
1630 Stmt::Compute { targets, expr, size_error, pos }
1631 }
1632 "ADD" if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() => self.corresponding(CorrespondingVerb::Add, pos)?,
1633 "SUBTRACT" if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() => self.corresponding(CorrespondingVerb::Subtract, pos)?,
1634 "ADD" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" => Stmt::Arith(Box::new(self.arith(&verb, pos)?)),
1635 "IF" => {
1636 let cond = self.cond()?;
1637 self.accept_word("THEN");
1638 let then = self.block(&["ELSE", "END-IF"])?;
1639 let otherwise = if self.accept_word("ELSE") { self.block(&["END-IF"])? } else { Vec::new() };
1640 self.accept_word("END-IF");
1641 Stmt::If { cond, then, otherwise, pos }
1642 }
1643 "PERFORM" => self.perform(pos)?,
1644 "DISPLAY" => {
1645 let mut items = Vec::new();
1646 let no_advancing_ahead = |p: &Self| p.is_word("NO") && p.word_at(1) == Some("ADVANCING");
1647 while self.starts_operand() && !no_advancing_ahead(self) {
1648 items.push(self.operand()?);
1649 }
1650 let upon = if self.accept_word("UPON") {
1651 let name = self.name("a mnemonic name")?;
1652 let device = self.mnemonics.iter().find(|(m, _)| *m == name).map_or_else(|| name.clone(), |(_, e)| e.clone());
1653 Some(Upon { name, device })
1654 } else {
1655 None
1656 };
1657 let no_advancing = self.accept_word("WITH") | no_advancing_ahead(self);
1658 if no_advancing {
1659 self.expect_word("NO")?;
1660 self.expect_word("ADVANCING")?;
1661 if self.is_word("UPON") {
1662 return Err(self.error("the end of DISPLAY: Enterprise COBOL takes UPON before WITH NO ADVANCING"));
1663 }
1664 }
1665 self.accept_word("END-DISPLAY");
1666 Stmt::Display { items, upon, no_advancing, pos }
1667 }
1668 "INITIALIZE" => self.initialize(pos)?,
1669 "CALL" => Stmt::Call(Box::new(self.call(pos)?)),
1670 "INVOKE" => Stmt::Invoke(Box::new(self.invoke(pos)?)),
1671 "JSON" if self.accept_word("GENERATE") => Stmt::JsonGenerate(Box::new(self.json_generate(pos)?)),
1672 "JSON" if self.accept_word("PARSE") => Stmt::JsonParse(Box::new(self.json_parse(pos)?)),
1673 "XML" if self.accept_word("PARSE") => Stmt::XmlParse(Box::new(self.xml_parse(pos)?)),
1674 "XML" if self.accept_word("GENERATE") => Stmt::XmlGenerate(Box::new(self.xml_generate(pos)?)),
1675 "CANCEL" => {
1676 let mut targets = Vec::new();
1677 while self.starts_operand() {
1678 targets.push(self.operand()?);
1679 }
1680 if targets.is_empty() {
1681 return Err(self.error("a program to CANCEL"));
1682 }
1683 Stmt::Cancel { targets, pos }
1684 }
1685 "SET" => Stmt::Set { set: self.set()?, pos },
1686 "STRING" => Stmt::String(Box::new(self.string(pos)?)),
1687 "UNSTRING" => Stmt::Unstring(Box::new(self.unstring(pos)?)),
1688 "INSPECT" => Stmt::Inspect(Box::new(self.inspect(pos)?)),
1689 "SEARCH" => Stmt::Search(Box::new(self.search(pos)?)),
1690 "SORT" | "MERGE" | "RELEASE" | "RETURN" => Stmt::Sorting(Box::new(self.sorting(&verb, pos)?)),
1691 "NEXT" => {
1692 self.expect_word("SENTENCE")?;
1693 Stmt::NextSentence
1694 }
1695 "ACCEPT" => {
1696 let target = self.reference()?;
1697 if self.is_word("FROM") && self.word_at(1) == Some("ENVIRONMENT") {
1698 return Err(crate::messages::IWS0055.at(pos, "ACCEPT ... FROM ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's"));
1699 }
1700 let from = if self.accept_word("FROM") {
1701 let at = self.pos();
1702 match self.name("SYSIN, SYSIPT, CONSOLE, a mnemonic-name for one, DATE, DAY, DAY-OF-WEEK or TIME")?.as_str() {
1703 "DATE" => AcceptFrom::Date { four_digit_year: self.accept_word("YYYYMMDD") },
1704 "DAY" => AcceptFrom::Day { four_digit_year: self.accept_word("YYYYDDD") },
1705 "DAY-OF-WEEK" => AcceptFrom::DayOfWeek,
1706 "TIME" => AcceptFrom::Time,
1707 "COMMAND-LINE" => AcceptFrom::CommandLine,
1708 "ARGUMENT-NUMBER" => AcceptFrom::ArgumentNumber,
1709 "ARGUMENT-VALUE" => AcceptFrom::ArgumentValue,
1710 name => {
1711 self.accept_device(name, at)?;
1712 AcceptFrom::Sysin
1713 }
1714 }
1715 } else {
1716 AcceptFrom::Sysin
1717 };
1718 let [exception] = self.on_phrases(&["ON", "EXCEPTION"], &["END-ACCEPT"], |p| {
1719 p.accept_word("ON");
1720 p.expect_word("EXCEPTION")?;
1721 Ok(0)
1722 })?;
1723 self.accept_word("END-ACCEPT");
1724 Stmt::Accept { target, from, exception, pos }
1725 }
1726 "OPEN" => {
1727 let mut files = Vec::new();
1728 while let Some(mode) = self.accept_any(&["INPUT", "OUTPUT", "EXTEND", "I-O"]) {
1729 let mode = match mode.as_str() {
1730 "INPUT" => OpenMode::Input,
1731 "OUTPUT" => OpenMode::Output,
1732 "EXTEND" => OpenMode::Extend,
1733 _ => OpenMode::InputOutput,
1734 };
1735 while self.starts_ref() {
1736 files.push((mode, self.name("a file name")?));
1737 self.accept_any(&["REVERSED"]);
1738 if self.accept_word("WITH") || self.is_word("NO") {
1739 self.expect_word("NO")?;
1740 self.expect_word("REWIND")?;
1741 }
1742 }
1743 }
1744 if files.is_empty() {
1745 return Err(self.error("INPUT, OUTPUT, EXTEND or I-O and a file"));
1746 }
1747 Stmt::Open { files, pos }
1748 }
1749 "CLOSE" => {
1750 let mut files = Vec::new();
1751 while self.starts_ref() {
1752 let name = self.name("a file name")?;
1753 let closing = if self.accept_any(&["REEL", "UNIT"]).is_some() {
1754 if self.accept_word("FOR") {
1755 self.expect_word("REMOVAL")?;
1756 } else if self.accept_word("WITH") || self.is_word("NO") {
1757 self.expect_word("NO")?;
1758 self.expect_word("REWIND")?;
1759 }
1760 Some(Closing::Volume)
1761 } else {
1762 self.accept_word("WITH");
1763 if self.accept_word("LOCK") {
1764 Some(Closing::Lock)
1765 } else if self.accept_word("NO") {
1766 self.expect_word("REWIND")?;
1767 Some(Closing::NoRewind)
1768 } else {
1769 None
1770 }
1771 };
1772 files.push((name, closing));
1773 }
1774 Stmt::Close { files, pos }
1775 }
1776 "READ" => {
1777 let file = self.name("a file name")?;
1778 let previous = self.accept_word("PREVIOUS");
1779 let next = previous || self.accept_word("NEXT");
1780 self.accept_word("RECORD");
1781 let into = if self.accept_word("INTO") { Some(self.reference()?) } else { None };
1782 let key = if self.accept_word("KEY") {
1783 self.accept_word("IS");
1784 Some(self.reference()?)
1785 } else {
1786 None
1787 };
1788 let [at_end, invalid] = self.on_phrases(&["AT", "END", "INVALID"], &["END-READ"], |p| {
1789 if p.accept_word("INVALID") {
1790 p.accept_word("KEY");
1791 return Ok(1);
1792 }
1793 p.accept_word("AT");
1794 p.expect_word("END").map(|()| 0)
1795 })?;
1796 self.accept_word("END-READ");
1797 Stmt::Read(Box::new(ReadStmt { file, next, previous, into, key, at_end, invalid, pos }))
1798 }
1799 "REWRITE" => {
1800 let record = self.reference()?;
1801 let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
1802 let invalid = self.invalid_key("END-REWRITE")?;
1803 Stmt::Rewrite { record, from, invalid, pos }
1804 }
1805 "DELETE" => {
1806 let file = self.name("a file name")?;
1807 self.accept_word("RECORD");
1808 let invalid = self.invalid_key("END-DELETE")?;
1809 Stmt::Delete { file, invalid, pos }
1810 }
1811 "START" => {
1812 let file = self.name("a file name")?;
1813 let key = if self.accept_word("KEY") {
1814 self.accept_word("IS");
1815 let op = if self.accept_word("NOT") {
1816 match self.relop()? {
1817 Some(RelOp::Lt) => RelOp::Ge,
1818 _ => return Err(self.error("NOT < in START KEY")),
1819 }
1820 } else {
1821 self.relop()?.ok_or_else(|| self.error("a relation after START KEY"))?
1822 };
1823 Some((op, self.reference()?))
1824 } else {
1825 None
1826 };
1827 let invalid = self.invalid_key("END-START")?;
1828 Stmt::Start { file, key, invalid, pos }
1829 }
1830 "WRITE" => {
1831 let record = self.reference()?;
1832 let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
1833 let mut advancing = None;
1834 if let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
1835 let before = side == "BEFORE";
1836 self.accept_word("ADVANCING");
1837 let mnemonic = self.word().and_then(|w| self.mnemonics.iter().find(|(name, environment)| name == w && advancing_environment_name(environment))).cloned();
1838 advancing = Some(if self.accept_word("PAGE") {
1839 Advancing::Page { before }
1840 } else if let Some((name, environment)) = mnemonic {
1841 self.at += 1;
1842 Advancing::Mnemonic { before, name, environment }
1843 } else {
1844 let count = self.expr()?;
1845 self.accept_any(&["LINE", "LINES"]);
1846 Advancing::Lines { before, count }
1847 });
1848 }
1849 let eop_ahead = |p: &Self, i: usize| {
1850 let at = usize::from(p.word_at(i) == Some("AT"));
1851 matches!(p.word_at(i + at), Some("END-OF-PAGE" | "EOP"))
1852 };
1853 let [end_of_page, invalid] = self.phrases_opening(|p, i| eop_ahead(p, i) || p.word_at(i) == Some("INVALID"), &["END-WRITE"], |p| {
1854 if p.accept_word("INVALID") {
1855 p.accept_word("KEY");
1856 return Ok(1);
1857 }
1858 p.accept_word("AT");
1859 p.at += 1;
1860 Ok(0)
1861 })?;
1862 self.accept_word("END-WRITE");
1863 Stmt::Write { record, from, advancing, invalid, end_of_page, pos }
1864 }
1865 "GO" => {
1866 self.accept_word("TO");
1867 if self.peek() == Some(&Tok::Period) {
1868 return Ok(Stmt::GoTo { target: None, pos });
1869 }
1870 let target = self.proc_name()?;
1871 if !self.starts_proc_name() && !self.is_word("DEPENDING") {
1872 return Ok(Stmt::GoTo { target: Some(target), pos });
1873 }
1874 let mut targets = vec![target];
1875 while self.starts_proc_name() && !self.is_word("DEPENDING") {
1876 targets.push(self.proc_name()?);
1877 }
1878 self.expect_word("DEPENDING")?;
1879 self.accept_word("ON");
1880 Stmt::GoToDepending { targets, on: self.reference()?, pos }
1881 }
1882 "ALTER" => {
1883 let mut pairs = Vec::new();
1884 loop {
1885 let paragraph = self.proc_name()?;
1886 self.expect_word("TO")?;
1887 if self.accept_word("PROCEED") {
1888 self.expect_word("TO")?;
1889 }
1890 pairs.push((paragraph, self.proc_name()?));
1891 if !self.starts_proc_name() {
1892 break;
1893 }
1894 }
1895 Stmt::Alter { pairs, pos }
1896 }
1897 "ENTRY" => {
1898 let Some(Tok::Alnum(name)) = self.peek().cloned() else {
1899 return Err(self.error("an alphanumeric literal naming the entry point"));
1900 };
1901 self.at += 1;
1902 let using = if self.accept_word("USING") { self.parameters()? } else { Vec::new() };
1903 Stmt::Entry { name: name.to_ascii_uppercase(), using, pos }
1904 }
1905 "EVALUATE" => self.evaluate(pos)?,
1906 "INITIATE" | "GENERATE" | "TERMINATE" | "SUPPRESS" => Stmt::Report(Box::new(self.report_statement(&verb, pos)?)),
1907 "GOBACK" => Stmt::Goback { pos },
1908 "STOP" if self.accept_word("RUN") => Stmt::StopRun { pos },
1909 "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, pos },
1911 "STOP" => return Err(self.error("RUN or a literal after STOP")),
1912 "CONTINUE" => Stmt::Continue,
1913 "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")),
1914 "EXIT" => match self.accept_any(&["PROGRAM", "PARAGRAPH", "SECTION", "PERFORM", "METHOD"]).as_deref() {
1915 Some("PROGRAM") => Stmt::ExitProgram { pos },
1916 Some("METHOD") => Stmt::ExitMethod { pos },
1917 Some("PARAGRAPH") => Stmt::Exit { kind: ExitKind::Paragraph, pos },
1918 Some("SECTION") => Stmt::Exit { kind: ExitKind::Section, pos },
1919 Some(_) if self.accept_word("CYCLE") => Stmt::Exit { kind: ExitKind::PerformCycle, pos },
1920 Some(_) => Stmt::Exit { kind: ExitKind::Perform, pos },
1921 None => Stmt::Exit { kind: ExitKind::Plain, pos },
1922 },
1923 other => return Err(crate::messages::IWR0002.at(pos, format!("{other} is not a statement ironwork for COBOL supports yet"))),
1924 })
1925 }
1926
1927 fn size_error(&mut self) -> R<Option<SizeError>> {
1928 let [h] = self.on_phrases(&["ON", "SIZE"], &["END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"], |p| {
1929 p.accept_word("ON");
1930 p.expect_word("SIZE")?;
1931 p.expect_word("ERROR").map(|()| 0)
1932 })?;
1933 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() }))
1934 }
1935
1936 fn corresponding(&mut self, verb: CorrespondingVerb, pos: Pos) -> R<Stmt> {
1939 let from = self.reference()?;
1940 self.expect_word(if verb == CorrespondingVerb::Subtract { "FROM" } else { "TO" })?;
1941 let to = self.reference()?;
1942 let (rounded, size_error) = match verb {
1943 CorrespondingVerb::Move => (false, None),
1944 _ => (self.accept_word("ROUNDED"), self.size_error()?),
1945 };
1946 if self.starts_ref() {
1947 return Err(crate::messages::IWS0057.at(self.pos(), "CORRESPONDING takes one receiving group"));
1948 }
1949 self.accept_any(&["END-ADD", "END-SUBTRACT"]);
1950 Ok(Stmt::Corresponding(Box::new(Corresponding { verb, from, to, rounded, size_error, pos })))
1951 }
1952
1953 fn targets(&mut self) -> R<Vec<Target>> {
1954 let mut out = Vec::new();
1955 while self.starts_ref() {
1956 let r = self.reference()?;
1957 out.push(Target { r, rounded: self.accept_word("ROUNDED") });
1958 }
1959 if out.is_empty() {
1960 return Err(self.error("a receiving item"));
1961 }
1962 Ok(out)
1963 }
1964
1965 fn refs(&mut self) -> R<Vec<Ref>> {
1966 let mut out = Vec::new();
1967 while self.starts_ref() {
1968 out.push(self.reference()?);
1969 }
1970 if out.is_empty() {
1971 return Err(self.error("a data name"));
1972 }
1973 Ok(out)
1974 }
1975
1976 fn operands_until(&mut self, stops: &[&str]) -> R<Vec<Expr>> {
1977 let mut out = Vec::new();
1978 while self.starts_operand() && !self.word().is_some_and(|w| stops.contains(&w)) {
1979 out.push(Expr::Operand(self.operand()?));
1980 }
1981 if out.is_empty() {
1982 return Err(self.error("an operand"));
1983 }
1984 Ok(out)
1985 }
1986
1987 fn arith(&mut self, verb: &str, pos: Pos) -> R<Arith> {
1988 let sum = |mut es: Vec<Expr>| {
1989 let first = es.remove(0);
1990 es.into_iter().fold(first, |acc, e| Expr::Bin(Box::new(acc), BinOp::Add, Box::new(e)))
1991 };
1992 let of = |t: &Target| Expr::Operand(Operand::Ref(t.r.clone()));
1993 let bin = |a: Expr, op: BinOp, b: Expr| Expr::Bin(Box::new(a), op, Box::new(b));
1994 let mut remainder = None;
1995 let (verb, computations) = match verb {
1996 "ADD" => {
1997 let addends = sum(self.operands_until(&["TO", "GIVING"])?);
1998 if self.accept_word("TO") {
1999 if self.words_ahead_include("GIVING") {
2000 let to = sum(self.operands_until(&["GIVING"])?);
2001 self.expect_word("GIVING")?;
2002 let targets = self.targets()?;
2003 let total = bin(addends, BinOp::Add, to);
2004 (ArithVerb::Add, targets.into_iter().map(|t| (t, total.clone())).collect())
2005 } else {
2006 let targets = self.targets()?;
2007 (ArithVerb::Add, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Add, addends.clone()))).collect())
2008 }
2009 } else {
2010 self.expect_word("GIVING")?;
2011 let targets = self.targets()?;
2012 (ArithVerb::Add, targets.into_iter().map(|t| (t, addends.clone())).collect())
2013 }
2014 }
2015 "SUBTRACT" => {
2016 let subtrahend = sum(self.operands_until(&["FROM"])?);
2017 self.expect_word("FROM")?;
2018 if self.words_ahead_include("GIVING") {
2019 let minuend = sum(self.operands_until(&["GIVING"])?);
2020 self.expect_word("GIVING")?;
2021 let targets = self.targets()?;
2022 let diff = bin(minuend, BinOp::Sub, subtrahend);
2023 (ArithVerb::Subtract, targets.into_iter().map(|t| (t, diff.clone())).collect())
2024 } else {
2025 let targets = self.targets()?;
2026 (ArithVerb::Subtract, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Sub, subtrahend.clone()))).collect())
2027 }
2028 }
2029 "MULTIPLY" => {
2030 let a = Expr::Operand(self.operand()?);
2031 self.expect_word("BY")?;
2032 if self.words_ahead_include("GIVING") {
2033 let b = Expr::Operand(self.operand()?);
2034 self.expect_word("GIVING")?;
2035 let targets = self.targets()?;
2036 (ArithVerb::Multiply, targets.into_iter().map(|t| (t, bin(a.clone(), BinOp::Mul, b.clone()))).collect())
2037 } else {
2038 let targets = self.targets()?;
2039 (ArithVerb::Multiply, targets.into_iter().map(|t| (t.clone(), bin(a.clone(), BinOp::Mul, of(&t)))).collect())
2040 }
2041 }
2042 _ => {
2043 let first = Expr::Operand(self.operand()?);
2044 let into = match self.accept_any(&["INTO", "BY"]).as_deref() {
2045 Some("INTO") => true,
2046 Some(_) => false,
2047 None => return Err(self.error("INTO or BY")),
2048 };
2049 if !into || self.words_ahead_include("GIVING") {
2050 let second = Expr::Operand(self.operand()?);
2051 let (dividend, divisor) = if into { (second, first) } else { (first, second) };
2052 self.expect_word("GIVING")?;
2053 let targets = self.targets()?;
2054 if self.accept_word("REMAINDER") {
2055 let r = self.reference()?;
2056 remainder = Some((Target { r, rounded: false }, dividend.clone(), divisor.clone()));
2057 }
2058 (ArithVerb::Divide, targets.into_iter().map(|t| (t, bin(dividend.clone(), BinOp::Div, divisor.clone()))).collect())
2059 } else {
2060 let targets = self.targets()?;
2061 (ArithVerb::Divide, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Div, first.clone()))).collect())
2062 }
2063 }
2064 };
2065 let size_error = self.size_error()?;
2066 self.accept_any(&["END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"]);
2067 Ok(Arith { verb, computations, remainder, size_error, pos })
2068 }
2069
2070 fn words_ahead_include(&self, word: &str) -> bool {
2072 self.tokens[self.at..]
2073 .iter()
2074 .take_while(|t| t.tok != Tok::Period && !matches!(&t.tok, Tok::Word(w) if VERBS.contains(&w.as_str()) && w != word))
2075 .any(|t| matches!(&t.tok, Tok::Word(w) if w == word))
2076 }
2077
2078 fn perform(&mut self, pos: Pos) -> R<Stmt> {
2079 let named = (self.word().is_some_and(|w| !VERBS.contains(&w) && !PHRASE_WORDS.contains(&w) && w != "TEST") || self.peek().is_some_and(digits))
2080 && !self.times_ahead();
2081 if named {
2082 let from = self.proc_name()?;
2083 let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.proc_name()?) } else { None };
2084 let repeat = self.repeat()?;
2085 return Ok(Stmt::PerformProc { from, thru, repeat, pos });
2086 }
2087 let repeat = self.repeat()?;
2088 if matches!(&repeat, Loop::Varying { after, .. } if !after.is_empty()) {
2089 return Err(crate::messages::IWS0058.at(pos, "an inline PERFORM cannot have AFTER phrases: Enterprise COBOL takes them only when PERFORM names a procedure"));
2090 }
2091 let body = self.block(&["END-PERFORM"])?;
2092 self.expect_word("END-PERFORM")?;
2093 Ok(Stmt::PerformInline { body, repeat, pos })
2094 }
2095
2096 fn on_phrases<const N: usize>(&mut self, starts: &[&str], ends: &[&str], head: impl Fn(&mut Self) -> R<usize>) -> R<[Handlers; N]> {
2098 let opens = |p: &Self, i: usize| match p.word_at(i) {
2100 Some(w @ ("ON" | "AT")) if starts.contains(&w) => p.word_at(i + 1).is_some_and(|k| k != w && starts.contains(&k)),
2101 Some(w) => starts.contains(&w),
2102 None => false,
2103 };
2104 self.phrases_opening(opens, ends, head)
2105 }
2106
2107 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]> {
2109 let stops = [&["NOT"][..], ends].concat();
2110 let mut handlers = std::array::from_fn(|_| Handlers::default());
2111 loop {
2112 let negated = self.is_word("NOT") && opens(self, 1);
2113 if !negated && !opens(self, 0) {
2114 return Ok(handlers);
2115 }
2116 self.at += usize::from(negated);
2117 let h = &mut handlers[head(self)?];
2118 let body = self.block(&stops)?;
2119 if negated { h.not_on = Some(body) } else { h.on = Some(body) }
2120 }
2121 }
2122
2123 fn invalid_key(&mut self, end: &str) -> R<Handlers> {
2125 let [h] = self.on_phrases(&["INVALID"], &[end], |p| {
2126 p.at += 1;
2127 p.accept_word("KEY");
2128 Ok(0)
2129 })?;
2130 self.accept_word(end);
2131 Ok(h)
2132 }
2133
2134 fn json_operand_follows(&self) -> bool {
2135 self.starts_ref() && !self.word().is_some_and(|w| JSON_PHRASES.contains(&w))
2136 }
2137
2138 fn figurative_list(&mut self) -> R<Vec<Figurative>> {
2139 let mut out = Vec::new();
2140 loop {
2141 let word = self.name("a figurative constant")?;
2142 out.push(figurative(&word).filter(|f| !matches!(f, Figurative::Quote | Figurative::Null)).ok_or_else(|| self.error("ZERO, SPACE, LOW-VALUE or HIGH-VALUE"))?);
2143 if !self.accept_word("OR") {
2144 return Ok(out);
2145 }
2146 }
2147 }
2148
2149 fn marker(&mut self, inside: bool) -> R<(Marker, Option<Ref>)> {
2151 if self.starts_ref() {
2152 return Ok((Marker::Condition(self.reference()?), None));
2153 }
2154 let literal = self.literal()?;
2155 let item = if inside {
2156 self.expect_word("IN")?;
2157 Some(self.reference()?)
2158 } else {
2159 None
2160 };
2161 Ok((Marker::Literal(literal), item))
2162 }
2163
2164 fn json_generate(&mut self, pos: Pos) -> R<JsonGenerate> {
2165 let receiver = self.reference()?;
2166 self.expect_word("FROM")?;
2167 let from = self.reference()?;
2168 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 };
2169 loop {
2170 if self.accept_word("COUNT") {
2171 self.accept_word("IN");
2172 g.count = Some(self.reference()?);
2173 } else if self.accept_word("INDICATING") {
2174 loop {
2175 let item = self.reference()?;
2176 self.accept_word("IS");
2177 self.accept_word("JSON");
2178 self.expect_word("NULL")?;
2179 self.expect_word("USING")?;
2180 let (marker, indicator) = self.marker(true)?;
2181 g.indicating.push(NullIndicator { item, marker, indicator });
2182 if !self.accept_word("ALSO") {
2183 break;
2184 }
2185 }
2186 } else if self.accept_word("ENCODING") {
2187 g.encoding = Some(if self.accept_word("FROM") {
2188 self.expect_word("CODEPAGE")?;
2189 Encoding::FromCodepage
2190 } else {
2191 Encoding::Ccsid(self.operand()?)
2192 });
2193 } else if self.accept_word("NAME") {
2194 self.accept_word("OF");
2195 loop {
2196 let item = self.reference()?;
2197 self.accept_word("IS");
2198 let name = if self.accept_word("OMITTED") { None } else { Some(self.literal()?) };
2199 g.names.push((item, name));
2200 if !self.json_operand_follows() {
2201 break;
2202 }
2203 }
2204 } else if self.accept_word("SUPPRESS") {
2205 loop {
2206 if self.accept_word("EVERY") {
2207 let numeric = match self.accept_any(&["NUMERIC", "NONNUMERIC"]).as_deref() {
2208 Some("NUMERIC") => Some(true),
2209 Some(_) => Some(false),
2210 None => None,
2211 };
2212 self.expect_word("WHEN")?;
2213 g.suppress.push(Suppression::Every { numeric, form: None, when: self.figurative_list()? });
2214 } else {
2215 let item = self.reference()?;
2216 let when = if self.accept_word("WHEN") { self.figurative_list()? } else { Vec::new() };
2217 g.suppress.push(Suppression::Item { item, when });
2218 }
2219 if !(self.is_word("EVERY") || self.json_operand_follows()) {
2220 break;
2221 }
2222 }
2223 } else if self.accept_word("CONVERTING") {
2224 loop {
2225 let item = self.reference()?;
2226 self.expect_word("TO")?;
2227 self.accept_word("JSON");
2228 let conversion = if self.accept_word("NULL") {
2229 self.expect_word("USING")?;
2230 let [f] = self.figurative_list()?[..] else { return Err(self.error("one figurative constant after USING")) };
2231 JsonConversion::Null(f)
2232 } else {
2233 if self.accept_any(&["BOOLEAN", "BOOL"]).is_none() {
2234 return Err(self.error("BOOLEAN or NULL"));
2235 }
2236 self.expect_word("USING")?;
2237 JsonConversion::Boolean(self.marker(false)?.0)
2238 };
2239 g.converting.push((item, conversion));
2240 if !self.accept_word("ALSO") {
2241 break;
2242 }
2243 }
2244 } else {
2245 break;
2246 }
2247 }
2248 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-JSON"], |p| {
2249 p.accept_word("ON");
2250 p.expect_word("EXCEPTION")?;
2251 Ok(0)
2252 })?;
2253 self.accept_word("END-JSON");
2254 (g.on_exception, g.not_on_exception) = (h.on, h.not_on);
2255 Ok(g)
2256 }
2257
2258 fn flag(&mut self, inside: bool) -> R<(Flag, Option<Ref>)> {
2261 self.expect_word("USING")?;
2262 if self.starts_ref() {
2263 let first = self.reference()?;
2264 if self.accept_word("AND") {
2265 return Ok((Flag::Conditions(first, self.reference()?), None));
2266 }
2267 return Ok((Flag::Condition(first), None));
2268 }
2269 let on = self.literal()?;
2270 self.expect_word("AND")?;
2271 let off = self.literal()?;
2272 let item = if inside {
2273 self.expect_word("IN")?;
2274 Some(self.reference()?)
2275 } else {
2276 None
2277 };
2278 Ok((Flag::Literals(on, off), item))
2279 }
2280
2281 fn json_parse(&mut self, pos: Pos) -> R<JsonParse> {
2282 let source = self.reference()?;
2283 self.expect_word("INTO")?;
2284 let into = self.reference()?;
2285 let mut j = JsonParse {
2286 source,
2287 into,
2288 detail: false,
2289 ignoring: Vec::new(),
2290 indicating: Vec::new(),
2291 encoding: None,
2292 names: Vec::new(),
2293 suppress: Vec::new(),
2294 converting: Vec::new(),
2295 on_exception: None,
2296 not_on_exception: None,
2297 pos,
2298 };
2299 loop {
2300 if self.accept_word("WITH") || self.is_word("DETAIL") {
2301 self.expect_word("DETAIL")?;
2302 j.detail = true;
2303 } else if self.accept_word("IGNORING") {
2304 loop {
2305 self.accept_word("JSON");
2306 self.expect_word("NULL")?;
2307 self.expect_word("FOR")?;
2308 j.ignoring.push(if self.accept_word("ALL") { None } else { Some(self.reference()?) });
2309 if !self.accept_word("ALSO") {
2310 break;
2311 }
2312 }
2313 } else if self.accept_word("INDICATING") {
2314 loop {
2315 let item = self.reference()?;
2316 self.accept_word("IS");
2317 self.accept_word("JSON");
2318 self.expect_word("NULL")?;
2319 let (flag, indicator) = self.flag(true)?;
2320 j.indicating.push((item, flag, indicator));
2321 if !self.accept_word("ALSO") {
2322 break;
2323 }
2324 }
2325 } else if self.accept_word("ENCODING") {
2326 j.encoding = Some(if self.accept_word("FROM") {
2327 self.expect_word("CODEPAGE")?;
2328 Encoding::FromCodepage
2329 } else {
2330 Encoding::Ccsid(self.operand()?)
2331 });
2332 } else if self.accept_word("NAME") {
2333 self.accept_word("OF");
2334 loop {
2335 let item = self.reference()?;
2336 self.accept_word("IS");
2337 let name = if self.accept_word("OMITTED") { None } else { Some(self.literal()?) };
2338 j.names.push((item, name));
2339 if !self.json_operand_follows() {
2340 break;
2341 }
2342 }
2343 } else if self.accept_word("SUPPRESS") {
2344 loop {
2345 j.suppress.push(self.reference()?);
2346 if !self.json_operand_follows() {
2347 break;
2348 }
2349 }
2350 } else if self.accept_word("CONVERTING") {
2351 loop {
2352 let item = self.reference()?;
2353 self.expect_word("FROM")?;
2354 self.accept_word("JSON");
2355 let conversion = if self.accept_word("NULL") {
2356 self.expect_word("USING")?;
2357 let [f] = self.figurative_list()?[..] else { return Err(self.error("one figurative constant after USING")) };
2358 ParseConversion::Null(f)
2359 } else {
2360 if self.accept_any(&["BOOLEAN", "BOOL"]).is_none() {
2361 return Err(self.error("BOOLEAN or NULL"));
2362 }
2363 ParseConversion::Boolean(Box::new(self.flag(false)?.0))
2364 };
2365 j.converting.push((item, conversion));
2366 if !self.accept_word("ALSO") {
2367 break;
2368 }
2369 }
2370 } else {
2371 break;
2372 }
2373 }
2374 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-JSON"], |p| {
2375 p.accept_word("ON");
2376 p.expect_word("EXCEPTION")?;
2377 Ok(0)
2378 })?;
2379 self.accept_word("END-JSON");
2380 (j.on_exception, j.not_on_exception) = (h.on, h.not_on);
2381 Ok(j)
2382 }
2383
2384 fn xml_operand_follows(&self) -> bool {
2385 self.starts_ref() && !self.word().is_some_and(|w| XML_PHRASES.contains(&w))
2386 }
2387
2388 fn xml_form(&mut self) -> R<Option<XmlForm>> {
2389 Ok(match self.accept_any(&["ATTRIBUTE", "ELEMENT", "CONTENT"]).as_deref() {
2390 Some("ATTRIBUTE") => Some(XmlForm::Attribute),
2391 Some("ELEMENT") => Some(XmlForm::Element),
2392 Some(_) => Some(XmlForm::Content),
2393 None => None,
2394 })
2395 }
2396
2397 fn xml_generate(&mut self, pos: Pos) -> R<XmlGenerate> {
2398 let receiver = self.reference()?;
2399 self.expect_word("FROM")?;
2400 let from = self.reference()?;
2401 let mut x = XmlGenerate {
2402 receiver,
2403 from,
2404 count: None,
2405 encoding: None,
2406 declaration: false,
2407 attributes: false,
2408 namespace: None,
2409 prefix: None,
2410 names: Vec::new(),
2411 types: Vec::new(),
2412 suppress: Vec::new(),
2413 on_exception: None,
2414 not_on_exception: None,
2415 pos,
2416 };
2417 loop {
2418 if self.accept_word("COUNT") {
2419 self.accept_word("IN");
2420 x.count = Some(self.reference()?);
2421 continue;
2422 }
2423 let with = self.accept_word("WITH");
2424 if self.accept_word("ENCODING") {
2425 x.encoding = Some(self.operand()?);
2426 } else if self.accept_word("XML-DECLARATION") {
2427 x.declaration = true;
2428 } else if self.accept_word("ATTRIBUTES") {
2429 x.attributes = true;
2430 } else if with {
2431 return Err(self.error("ENCODING, XML-DECLARATION or ATTRIBUTES"));
2432 } else if self.accept_word("NAMESPACE") {
2433 self.accept_word("IS");
2434 x.namespace = Some(self.operand()?);
2435 if self.accept_word("NAMESPACE-PREFIX") {
2436 self.accept_word("IS");
2437 x.prefix = Some(self.operand()?);
2438 }
2439 } else if self.accept_word("NAME") {
2440 self.accept_word("OF");
2441 loop {
2442 let item = self.reference()?;
2443 self.accept_word("IS");
2444 x.names.push((item, self.literal()?));
2445 if !self.xml_operand_follows() {
2446 break;
2447 }
2448 }
2449 } else if self.accept_word("TYPE") {
2450 self.accept_word("OF");
2451 loop {
2452 let item = self.reference()?;
2453 self.accept_word("IS");
2454 let form = self.xml_form()?.ok_or_else(|| self.error("ATTRIBUTE, ELEMENT or CONTENT"))?;
2455 x.types.push((item, form));
2456 if !self.xml_operand_follows() {
2457 break;
2458 }
2459 }
2460 } else if self.accept_word("SUPPRESS") {
2461 loop {
2462 if self.accept_word("EVERY") {
2463 let numeric = match self.accept_any(&["NUMERIC", "NONNUMERIC"]).as_deref() {
2464 Some("NUMERIC") => Some(true),
2465 Some(_) => Some(false),
2466 None => None,
2467 };
2468 let form = self.xml_form()?;
2469 if numeric.is_none() && form.is_none() {
2470 return Err(self.error("NUMERIC, NONNUMERIC, ATTRIBUTE, CONTENT or ELEMENT"));
2471 }
2472 self.expect_word("WHEN")?;
2473 x.suppress.push(Suppression::Every { numeric, form, when: self.figurative_list()? });
2474 } else {
2475 let item = self.reference()?;
2476 let when = if self.accept_word("WHEN") { self.figurative_list()? } else { Vec::new() };
2477 x.suppress.push(Suppression::Item { item, when });
2478 }
2479 if !(self.is_word("EVERY") || self.xml_operand_follows()) {
2480 break;
2481 }
2482 }
2483 } else {
2484 break;
2485 }
2486 }
2487 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-XML"], |p| {
2488 p.accept_word("ON");
2489 p.expect_word("EXCEPTION")?;
2490 Ok(0)
2491 })?;
2492 self.accept_word("END-XML");
2493 (x.on_exception, x.not_on_exception) = (h.on, h.not_on);
2494 Ok(x)
2495 }
2496
2497 fn xml_parse(&mut self, pos: Pos) -> R<XmlParse> {
2498 let document = self.reference()?;
2499 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 };
2500 loop {
2501 if self.accept_word("WITH") || self.is_word("ENCODING") {
2502 self.expect_word("ENCODING")?;
2503 x.encoding = Some(self.operand()?);
2504 } else if self.accept_word("RETURNING") {
2505 self.expect_word("NATIONAL")?;
2506 x.returning_national = true;
2507 } else if self.is_word("VALIDATING") {
2508 let at = self.pos();
2510 self.at += 1;
2511 self.accept_word("WITH");
2512 let schema = if self.accept_word("FILE") { format!("FILE {}", self.name("an XML schema name")?) } else { self.reference()?.name };
2513 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")));
2514 } else {
2515 break;
2516 }
2517 }
2518 self.expect_word("PROCESSING")?;
2519 self.expect_word("PROCEDURE")?;
2520 self.accept_word("IS");
2521 x.procedure = self.proc_name()?;
2522 if self.accept_any(&["THRU", "THROUGH"]).is_some() {
2523 x.thru = Some(self.proc_name()?);
2524 }
2525 let [h] = self.on_phrases(&["ON", "EXCEPTION"], &["END-XML"], |p| {
2526 p.accept_word("ON");
2527 p.expect_word("EXCEPTION")?;
2528 Ok(0)
2529 })?;
2530 self.accept_word("END-XML");
2531 (x.on_exception, x.not_on_exception) = (h.on, h.not_on);
2532 Ok(x)
2533 }
2534
2535 fn call(&mut self, pos: Pos) -> R<Call> {
2536 let target = self.operand()?;
2537 let mut using = Vec::new();
2538 if self.accept_word("USING") {
2539 let mut mode = ArgMode::Reference;
2540 loop {
2541 let by = self.accept_word("BY");
2542 if let Some(m) = self.accept_any(&["REFERENCE", "CONTENT", "VALUE"]) {
2543 mode = match m.as_str() {
2544 "CONTENT" => ArgMode::Content,
2545 "VALUE" => ArgMode::Value,
2546 _ => ArgMode::Reference,
2547 };
2548 } else if by {
2549 return Err(self.error("REFERENCE, CONTENT or VALUE after BY"));
2550 } else if self.accept_word("OMITTED") {
2551 using.push(Arg { mode, value: None });
2552 } else if self.starts_operand() {
2553 using.push(Arg { mode, value: Some(self.operand()?) });
2554 } else {
2555 break;
2556 }
2557 }
2558 }
2559 let returning = if self.accept_word("RETURNING") { Some(self.reference()?) } else { None };
2560 let [exception] = self.on_phrases(&["ON", "EXCEPTION", "OVERFLOW"], &["END-CALL"], |p| {
2561 p.accept_word("ON");
2562 p.accept_any(&["EXCEPTION", "OVERFLOW"]).map(|_| 0).ok_or_else(|| p.error("EXCEPTION or OVERFLOW"))
2563 })?;
2564 self.accept_word("END-CALL");
2565 Ok(Call { target, using, returning, on_exception: exception.on, not_on_exception: exception.not_on, pos })
2566 }
2567
2568 fn overflow(&mut self, end: &str) -> R<Handlers> {
2570 let [h] = self.on_phrases(&["ON", "OVERFLOW"], &[end], |p| {
2571 p.accept_word("ON");
2572 p.expect_word("OVERFLOW").map(|()| 0)
2573 })?;
2574 self.accept_word(end);
2575 Ok(h)
2576 }
2577
2578 fn string(&mut self, pos: Pos) -> R<StringStmt> {
2579 let mut sources = Vec::new();
2580 while !self.is_word("INTO") {
2581 let mut group = Vec::new();
2582 while self.starts_operand() && !self.is_word("DELIMITED") {
2583 group.push(self.operand()?);
2584 }
2585 if group.is_empty() {
2586 return Err(self.error("a sending item"));
2587 }
2588 let delimiter = if self.accept_word("DELIMITED") {
2589 self.accept_word("BY");
2590 if self.accept_word("SIZE") { Delimiter::Size } else { Delimiter::By(self.operand()?) }
2591 } else {
2592 Delimiter::Size
2593 };
2594 sources.extend(group.into_iter().map(|op| (op, delimiter.clone())));
2595 }
2596 self.expect_word("INTO")?;
2597 let into = self.reference()?;
2598 let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
2599 self.expect_word("POINTER")?;
2600 Some(self.reference()?)
2601 } else {
2602 None
2603 };
2604 let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-STRING")?;
2605 Ok(StringStmt { sources, into, pointer, on_overflow, not_on_overflow, pos })
2606 }
2607
2608 fn unstring(&mut self, pos: Pos) -> R<Unstring> {
2609 let source = self.reference()?;
2610 let mut delimiters = Vec::new();
2611 if self.accept_word("DELIMITED") {
2612 self.accept_word("BY");
2613 loop {
2614 let all = self.accept_word("ALL");
2615 delimiters.push((all, self.operand()?));
2616 if !self.accept_word("OR") {
2617 break;
2618 }
2619 }
2620 }
2621 self.expect_word("INTO")?;
2622 let mut into = Vec::new();
2623 while self.starts_ref() {
2624 let target = self.reference()?;
2625 let delimiter_in = if self.accept_word("DELIMITER") {
2626 self.accept_word("IN");
2627 Some(self.reference()?)
2628 } else {
2629 None
2630 };
2631 let count_in = if self.accept_word("COUNT") {
2632 self.accept_word("IN");
2633 Some(self.reference()?)
2634 } else {
2635 None
2636 };
2637 into.push(UnstringInto { target, delimiter_in, count_in });
2638 }
2639 if into.is_empty() {
2640 return Err(self.error("a receiving item after INTO"));
2641 }
2642 let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
2643 self.expect_word("POINTER")?;
2644 Some(self.reference()?)
2645 } else {
2646 None
2647 };
2648 let tallying = if self.accept_word("TALLYING") {
2649 self.accept_word("IN");
2650 Some(self.reference()?)
2651 } else {
2652 None
2653 };
2654 let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-UNSTRING")?;
2655 Ok(Unstring { source, delimiters, into, pointer, tallying, on_overflow, not_on_overflow, pos })
2656 }
2657
2658 fn bounds(&mut self) -> R<Vec<Bound>> {
2659 let mut bounds = Vec::new();
2660 while let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
2661 self.accept_word("INITIAL");
2662 bounds.push(Bound { after: side == "AFTER", value: self.operand()? });
2663 }
2664 Ok(bounds)
2665 }
2666
2667 fn initialize(&mut self, pos: Pos) -> R<Stmt> {
2671 let category = |p: &Self| p.word().and_then(DataCategory::from_word);
2672 let mut targets = Vec::new();
2673 while self.starts_ref() && category(self).is_none() && !self.is_word("FILLER") {
2674 targets.push(self.reference()?);
2675 }
2676 if targets.is_empty() {
2677 return Err(self.error("a data name"));
2678 }
2679 let twice = |c: DataCategory, phrase: &str| crate::messages::IWS0059.at(pos, format!("INITIALIZE: {} is named twice in the {phrase} phrase", c.word()));
2680 let mut with = InitializeWith::default();
2681 if self.is_word("FILLER") || self.is_word("WITH") && self.word_at(1) == Some("FILLER") {
2682 self.accept_word("WITH");
2683 self.at += 1;
2684 with.filler = true;
2685 }
2686 loop {
2687 let named = if self.accept_word("ALL") {
2688 DataCategory::ALL.to_vec()
2689 } else if let Some(c) = category(self) {
2690 self.at += 1;
2691 vec![c]
2692 } else {
2693 break;
2694 };
2695 self.expect_word("TO")?;
2696 self.expect_word("VALUE")?;
2697 for c in named {
2698 if with.value.contains(&c) {
2699 return Err(twice(c, "VALUE"));
2700 }
2701 with.value.push(c);
2702 }
2703 }
2704 if self.is_word("THEN") && self.word_at(1) == Some("REPLACING") {
2705 self.at += 1;
2706 }
2707 if self.accept_word("REPLACING") {
2708 while let Some(c) = category(self) {
2709 self.at += 1;
2710 self.accept_word("DATA");
2711 self.expect_word("BY")?;
2712 let by = self.operand()?;
2713 if with.replacing.iter().any(|(d, _)| *d == c) {
2714 return Err(twice(c, "REPLACING"));
2715 }
2716 with.replacing.push((c, by));
2717 }
2718 if with.replacing.is_empty() {
2719 return Err(self.error("a category after REPLACING"));
2720 }
2721 }
2722 let then = usize::from(self.is_word("THEN"));
2723 if self.word_at(then) == Some("TO") && self.word_at(then + 1) == Some("DEFAULT") {
2724 self.at += then + 2;
2725 with.default = true;
2726 }
2727 let with = (with != InitializeWith::default()).then(|| Box::new(with));
2728 Ok(Stmt::Initialize { targets, with, pos })
2729 }
2730
2731 fn inspect(&mut self, pos: Pos) -> R<Inspect> {
2732 let target = if self.is_word("FUNCTION") { self.operand()? } else { Operand::Ref(self.reference()?) };
2733 let (mut tallying, mut replacing, mut converting) = (Vec::new(), Vec::new(), None);
2734 if self.accept_word("TALLYING") {
2735 while self.starts_ref() && self.word_at(1) == Some("FOR") || self.starts_ref() && !self.is_word("REPLACING") && self.tally_counter_ahead() {
2736 let counter = self.reference()?;
2737 self.expect_word("FOR")?;
2738 loop {
2739 if self.accept_word("CHARACTERS") {
2740 tallying.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
2741 } else if let Some(mode) = self.accept_any(&["ALL", "LEADING"]) {
2742 let mode = if mode == "ALL" { InspectMode::All } else { InspectMode::Leading };
2743 loop {
2744 let pattern = self.operand()?;
2745 tallying.push(InspectPhrase { mode, pattern: Some(pattern), by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
2746 if !self.starts_operand() || self.word_at(1) == Some("FOR") || self.is_word("ALL") || self.is_word("LEADING") {
2747 break;
2748 }
2749 }
2750 } else {
2751 break;
2752 }
2753 }
2754 }
2755 }
2756 if self.accept_word("REPLACING") {
2757 loop {
2758 if self.accept_word("CHARACTERS") {
2759 self.expect_word("BY")?;
2760 let by = self.operand()?;
2761 replacing.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: Some(by), counter: None, bounds: self.bounds()? });
2762 } else if let Some(mode) = self.accept_any(&["ALL", "LEADING", "FIRST"]) {
2763 let mode = match mode.as_str() {
2764 "ALL" => InspectMode::All,
2765 "LEADING" => InspectMode::Leading,
2766 _ => InspectMode::First,
2767 };
2768 loop {
2769 let pattern = self.operand()?;
2770 self.expect_word("BY")?;
2771 let by = self.operand()?;
2772 replacing.push(InspectPhrase { mode, pattern: Some(pattern), by: Some(by), counter: None, bounds: self.bounds()? });
2773 if !self.starts_operand() || self.is_word("ALL") || self.is_word("LEADING") || self.is_word("FIRST") {
2774 break;
2775 }
2776 }
2777 } else {
2778 break;
2779 }
2780 }
2781 }
2782 if self.accept_word("CONVERTING") {
2783 let from = self.operand()?;
2784 self.expect_word("TO")?;
2785 let to = self.operand()?;
2786 converting = Some((from, to, self.bounds()?));
2787 }
2788 if tallying.is_empty() && replacing.is_empty() && converting.is_none() {
2789 return Err(self.error("TALLYING, REPLACING or CONVERTING"));
2790 }
2791 Ok(Inspect { target, tallying, replacing, converting, pos })
2792 }
2793
2794 fn tally_counter_ahead(&self) -> bool {
2796 let mut i = self.at + 1;
2797 if self.qualifying_paren_at(i) {
2798 let mut depth = 0;
2799 while let Some(t) = self.tokens.get(i) {
2800 match t.tok {
2801 Tok::LParen => depth += 1,
2802 Tok::RParen => {
2803 depth -= 1;
2804 if depth == 0 {
2805 i += 1;
2806 break;
2807 }
2808 }
2809 _ => {}
2810 }
2811 i += 1;
2812 }
2813 }
2814 matches!(self.tokens.get(i).map(|t| &t.tok), Some(Tok::Word(w)) if w == "FOR")
2815 }
2816
2817 fn search(&mut self, pos: Pos) -> R<Search> {
2818 let all = self.accept_word("ALL");
2819 let table = self.reference()?;
2820 let varying = if self.accept_word("VARYING") { Some(self.reference()?) } else { None };
2821 let at_end = if self.accept_word("AT") || self.is_word("END") {
2822 self.expect_word("END")?;
2823 Some(self.block(&["WHEN"])?)
2824 } else {
2825 None
2826 };
2827 let mut whens = Vec::new();
2828 while self.accept_word("WHEN") {
2829 let cond = self.cond()?;
2830 let body = self.block(&["WHEN", "END-SEARCH"])?;
2831 whens.push((cond, body));
2832 }
2833 if whens.is_empty() {
2834 return Err(self.error("WHEN"));
2835 }
2836 self.accept_word("END-SEARCH");
2837 Ok(Search { table, all, varying, at_end, whens, pos })
2838 }
2839
2840 fn accept_device(&self, name: &str, pos: Pos) -> R<()> {
2845 let device = self.mnemonics.iter().find(|(m, _)| m == name).map_or(name, |(_, e)| e.as_str());
2846 if ACCEPT_DEVICES.contains(&device) {
2847 return Ok(());
2848 }
2849 let why = if GNUCOBOL_ACCEPT_SOURCES.contains(&name) {
2850 "GnuCOBOL's, not Enterprise COBOL's".to_owned()
2851 } else if device == name {
2852 "neither an environment-name ACCEPT reads, SYSIN, SYSIPT or CONSOLE, nor a mnemonic-name for one".to_owned()
2853 } else {
2854 format!("a mnemonic-name for {device}, which ACCEPT does not read")
2855 };
2856 Err(crate::messages::IWS0060.at(pos, format!("ACCEPT ... FROM {name}: {why}")))
2857 }
2858
2859 fn set(&mut self) -> R<SetStmt> {
2860 if self.is_word("ENVIRONMENT") {
2861 return Err(crate::messages::IWS0061.at(self.pos(), "SET ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's"));
2862 }
2863 if self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
2864 let mut targets = Vec::new();
2865 while self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
2866 self.at += 2;
2867 targets.push(self.reference()?);
2868 }
2869 self.expect_word("TO")?;
2870 return Ok(SetStmt::AddressOf { targets, value: self.operand()? });
2871 }
2872 let targets = self.refs()?;
2873 if self.accept_word("TO") {
2874 if self.accept_word("TRUE") {
2875 return Ok(SetStmt::ConditionTrue(targets));
2876 }
2877 if self.accept_word("FALSE") {
2878 return Ok(SetStmt::ConditionFalse(targets));
2879 }
2880 if self.accept_word("ENTRY") {
2881 return Ok(SetStmt::Entry { targets, entry: self.operand()? });
2882 }
2883 if let Some(status) = self.accept_any(&["ON", "OFF"]) {
2884 let mut groups = vec![(targets, status == "ON")];
2885 while self.starts_ref() {
2886 let targets = self.refs()?;
2887 self.expect_word("TO")?;
2888 let status = self.accept_any(&["ON", "OFF"]).ok_or_else(|| self.error("ON or OFF"))?;
2889 groups.push((targets, status == "ON"));
2890 }
2891 return Ok(SetStmt::Switches(groups));
2892 }
2893 return Ok(SetStmt::To { targets, value: self.operand()? });
2894 }
2895 match self.accept_any(&["UP", "DOWN"]).as_deref() {
2896 Some(direction) => {
2897 self.expect_word("BY")?;
2898 Ok(SetStmt::UpDown { targets, down: direction == "DOWN", by: self.expr()? })
2899 }
2900 None => Err(self.error("TO, UP BY or DOWN BY")),
2901 }
2902 }
2903
2904 fn proc_name(&mut self) -> R<ProcName> {
2905 let name = self.procedure_word("a procedure name")?;
2906 let section = if self.accept_any(&["OF", "IN"]).is_some() { Some(self.procedure_word("a section name")?) } else { None };
2907 Ok(ProcName { name, section })
2908 }
2909
2910 fn procedure_word(&mut self, what: &str) -> R<String> {
2912 match self.peek() {
2913 Some(t) if digits(t) => {
2914 let Tok::Number(n) = t.clone() else { unreachable!() };
2915 self.at += 1;
2916 Ok(n)
2917 }
2918 _ => self.name(what),
2919 }
2920 }
2921
2922 fn starts_proc_name(&self) -> bool {
2923 self.starts_ref() || self.peek().is_some_and(digits) && !self.paragraph_header() && !self.section_header()
2924 }
2925
2926 fn times_ahead(&mut self) -> bool {
2928 let at = self.at;
2929 let times = self.starts_operand() && self.expr().is_ok() && self.is_word("TIMES");
2930 self.at = at;
2931 times
2932 }
2933
2934 fn evaluate(&mut self, pos: Pos) -> R<Stmt> {
2935 let mut subjects = vec![self.subject()?];
2936 while self.accept_word("ALSO") {
2937 subjects.push(self.subject()?);
2938 }
2939 let (mut whens, mut other) = (Vec::new(), Vec::new());
2940 while self.is_word("WHEN") {
2941 if self.word_at(1) == Some("OTHER") {
2942 self.at += 2;
2943 other = self.block(&["END-EVALUATE"])?;
2944 break;
2945 }
2946 let mut alternatives = Vec::new();
2947 while self.is_word("WHEN") && self.word_at(1) != Some("OTHER") {
2948 self.at += 1;
2949 let mut objects = Vec::new();
2950 for (k, subject) in subjects.iter().enumerate() {
2951 if k > 0 {
2952 self.expect_word("ALSO")?;
2953 }
2954 objects.push(self.object(subject)?);
2955 }
2956 alternatives.push(objects);
2957 }
2958 let body = self.block(&["WHEN", "END-EVALUATE"])?;
2959 whens.push(When { alternatives, body });
2960 }
2961 self.accept_word("END-EVALUATE");
2962 Ok(Stmt::Evaluate { subjects, whens, other, pos })
2963 }
2964
2965 fn subject(&mut self) -> R<Subject> {
2966 if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
2967 return Ok(Subject::Bool(b == "TRUE"));
2968 }
2969 let save = self.at;
2970 self.expr()?;
2971 let conditional = self.relop_ahead(0) || self.is_word("IS") || self.is_word("NOT")
2972 || self.word().is_some_and(|w| matches!(w, "NUMERIC" | "ALPHABETIC" | "ALPHABETIC-LOWER" | "ALPHABETIC-UPPER" | "DBCS" | "KANJI" | "POSITIVE" | "NEGATIVE" | "ZERO"));
2973 self.at = save;
2974 Ok(if conditional { Subject::Cond(self.cond()?) } else { Subject::Expr(self.expr()?) })
2975 }
2976
2977 fn object(&mut self, subject: &Subject) -> R<Object> {
2978 if self.accept_word("ANY") {
2979 return Ok(Object::Any);
2980 }
2981 if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
2982 return Ok(Object::Bool(b == "TRUE"));
2983 }
2984 if !matches!(subject, Subject::Expr(_)) {
2985 return Ok(Object::Cond(self.cond()?));
2986 }
2987 let not = self.accept_word("NOT");
2988 let from = self.expr()?;
2989 let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.expr()?) } else { None };
2990 Ok(Object::Value { not, from, thru })
2991 }
2992
2993 fn repeat(&mut self) -> R<Loop> {
2994 let mut test_after = false;
2995 if self.accept_word("WITH") || self.is_word("TEST") {
2996 self.expect_word("TEST")?;
2997 test_after = self.accept_any(&["BEFORE", "AFTER"]).as_deref() == Some("AFTER");
2998 }
2999 if self.accept_word("UNTIL") {
3000 return Ok(Loop::Until { cond: self.cond()?, test_after });
3001 }
3002 if self.accept_word("VARYING") {
3003 let varying = Box::new(self.varying()?);
3004 let mut after = Vec::new();
3005 while self.is_word("AFTER") {
3006 if after.len() == 6 {
3007 return Err(self.error("the end of the PERFORM: Enterprise COBOL takes at most six AFTER phrases"));
3008 }
3009 self.at += 1;
3010 after.push(self.varying()?);
3011 }
3012 return Ok(Loop::Varying { varying, after, test_after });
3013 }
3014 if self.times_ahead() {
3015 let count = self.expr()?;
3016 self.expect_word("TIMES")?;
3017 return Ok(Loop::Times(count));
3018 }
3019 Ok(Loop::Once)
3020 }
3021
3022 fn varying(&mut self) -> R<Varying> {
3024 let var = self.reference()?;
3025 self.expect_word("FROM")?;
3026 let from = self.expr()?;
3027 self.expect_word("BY")?;
3028 let by = self.expr()?;
3029 self.expect_word("UNTIL")?;
3030 Ok(Varying { var, from, by, until: self.cond()? })
3031 }
3032
3033 fn starts_ref(&self) -> bool {
3034 self.word().is_some_and(|w| !VERBS.contains(&w) && !PHRASE_WORDS.contains(&w) && figurative(w).is_none() && w != "FUNCTION")
3035 && !self.paragraph_header()
3036 }
3037
3038 fn starts_operand(&self) -> bool {
3039 match self.peek() {
3040 Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Dbcs(_)) => true,
3041 Some(Tok::Number(_)) => !self.paragraph_header(),
3042 Some(Tok::Word(w)) => {
3043 (figurative(w).is_some() || matches!(w.as_str(), "ALL" | "FUNCTION" | "LENGTH" | "ADDRESS" | "DFHRESP" | "DFHVALUE") || self.starts_ref()) && !self.paragraph_header()
3044 }
3045 _ => false,
3046 }
3047 }
3048
3049 fn function_call(&mut self, pos: Pos) -> R<Operand> {
3051 let name = self.name("a function name")?;
3052 let (mut args, mut modifier, mut all_subscripts) = (Vec::new(), None, Vec::new());
3053 if self.qualifying_paren_at(self.at) && !self.refmod_ahead() {
3054 self.at += 1;
3055 while !self.accept(&Tok::RParen) {
3056 if let Some(m) = self.accept_any(&["LEADING", "TRAILING"]) {
3057 modifier = Some(m);
3058 continue;
3059 }
3060 if self.all_subscript_ahead() {
3061 let (table, all) = self.table_with_all()?;
3062 all_subscripts.push((args.len(), all));
3063 args.push(Expr::Operand(Operand::Ref(table)));
3064 continue;
3065 }
3066 args.push(self.expr()?);
3067 }
3068 }
3069 let refmod = self.refmod()?;
3070 Ok(Operand::Function(FunctionCall { name, args, modifier, refmod, all_subscripts, pos }))
3071 }
3072
3073 fn operand(&mut self) -> R<Operand> {
3074 let pos = self.pos();
3075 match self.peek() {
3076 Some(Tok::Word(w)) if w == "FUNCTION" => {
3077 self.at += 1;
3078 self.function_call(pos)
3079 }
3080 Some(Tok::Word(w)) if self.intrinsics.contains(w) && !(w == "LENGTH" && self.word_at(1) == Some("OF")) => self.function_call(pos),
3081 Some(Tok::Word(w)) if w == "LENGTH" && self.word_at(1) == Some("OF") => {
3082 self.at += 2;
3083 Ok(Operand::LengthOf(self.reference()?))
3084 }
3085 Some(Tok::Word(w)) if w == "DFHRESP" && self.peek_at(1) == Some(&Tok::LParen) => {
3086 self.at += 2;
3087 let condition = self.name("a CICS condition")?;
3088 self.expect(&Tok::RParen, "')'")?;
3089 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")))?;
3090 Ok(Operand::Literal(Literal::Number(code.to_string())))
3091 }
3092 Some(Tok::Word(w)) if w == "DFHVALUE" && self.peek_at(1) == Some(&Tok::LParen) => {
3093 self.at += 2;
3094 let name = self.name("a CVDA value")?;
3095 self.expect(&Tok::RParen, "')'")?;
3096 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")))?;
3097 Ok(Operand::Literal(Literal::Number(value.to_string())))
3098 }
3099 Some(Tok::Word(w)) if w == "ADDRESS" && self.word_at(1) == Some("OF") => {
3100 self.at += 2;
3101 Ok(Operand::AddressOf(self.reference()?))
3102 }
3103 Some(Tok::Word(w)) if figurative(w).is_some() || w == "ALL" => Ok(Operand::Literal(self.literal()?)),
3104 Some(Tok::Word(_)) => Ok(Operand::Ref(self.reference()?)),
3105 Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Dbcs(_) | Tok::Number(_)) => Ok(Operand::Literal(self.literal()?)),
3106 _ => Err(self.error("an operand")),
3107 }
3108 }
3109
3110 fn refmod_ahead(&self) -> bool {
3112 let mut depth = 0;
3113 for t in &self.tokens[self.at..] {
3114 match t.tok {
3115 Tok::LParen => depth += 1,
3116 Tok::RParen => {
3117 depth -= 1;
3118 if depth == 0 {
3119 return false;
3120 }
3121 }
3122 Tok::Colon if depth == 1 => return true,
3123 Tok::Period => return false,
3124 _ => {}
3125 }
3126 }
3127 false
3128 }
3129
3130 fn refmod(&mut self) -> R<Option<RefMod>> {
3131 if !self.qualifying_paren_at(self.at) || !self.refmod_ahead() {
3132 return Ok(None);
3133 }
3134 self.at += 1;
3135 let start = Box::new(self.expr()?);
3136 self.expect(&Tok::Colon, "':'")?;
3137 let length = if self.peek() == Some(&Tok::RParen) { None } else { Some(Box::new(self.expr()?)) };
3138 self.expect(&Tok::RParen, "')'")?;
3139 Ok(Some(RefMod { start, length }))
3140 }
3141
3142 fn all_subscript_ahead(&self) -> bool {
3144 if !self.starts_ref() {
3145 return false;
3146 }
3147 let mut at = self.at + 1;
3148 while matches!(self.tokens.get(at).map(|t| &t.tok), Some(Tok::Word(w)) if w == "OF" || w == "IN") {
3149 at += 2;
3150 }
3151 if !self.qualifying_paren_at(at) {
3152 return false;
3153 }
3154 let mut depth = 0;
3155 for t in &self.tokens[at..] {
3156 match &t.tok {
3157 Tok::LParen => depth += 1,
3158 Tok::RParen if depth == 1 => return false,
3159 Tok::RParen => depth -= 1,
3160 Tok::Word(w) if depth == 1 && w == "ALL" => return true,
3161 Tok::Period => return false,
3162 _ => {}
3163 }
3164 }
3165 false
3166 }
3167
3168 fn table_with_all(&mut self) -> R<(Ref, Vec<usize>)> {
3171 let pos = self.pos();
3172 let name = self.name("a data name")?;
3173 let mut qualifiers = Vec::new();
3174 while self.accept_any(&["OF", "IN"]).is_some() {
3175 qualifiers.push(self.name("a qualifier")?);
3176 }
3177 self.expect(&Tok::LParen, "'('")?;
3178 let (mut subscripts, mut all) = (Vec::new(), Vec::new());
3179 while !self.accept(&Tok::RParen) {
3180 if self.accept_any(&["ALL"]).is_some() {
3181 all.push(subscripts.len());
3182 subscripts.push(Expr::Operand(Operand::Literal(Literal::Number("1".into()))));
3183 } else {
3184 subscripts.push(self.expr()?);
3185 }
3186 }
3187 let refmod = self.refmod()?;
3188 Ok((Ref { name, qualifiers, subscripts, refmod, pos }, all))
3189 }
3190
3191 fn reference(&mut self) -> R<Ref> {
3192 let pos = self.pos();
3193 let name = self.name("a data name")?;
3194 let mut qualifiers = Vec::new();
3195 while self.accept_any(&["OF", "IN"]).is_some() {
3196 qualifiers.push(self.name("a qualifier")?);
3197 }
3198 let mut subscripts = Vec::new();
3199 if self.qualifying_paren_at(self.at) && !self.refmod_ahead() {
3200 self.at += 1;
3201 while !self.accept(&Tok::RParen) {
3202 subscripts.push(self.expr()?);
3203 }
3204 }
3205 let refmod = self.refmod()?;
3206 Ok(Ref { name, qualifiers, subscripts, refmod, pos })
3207 }
3208
3209 fn expr(&mut self) -> R<Expr> {
3210 let mut left = self.term()?;
3211 loop {
3212 let op = match self.peek() {
3213 Some(Tok::Plus) => BinOp::Add,
3214 Some(Tok::Minus) => BinOp::Sub,
3215 _ => return Ok(left),
3216 };
3217 self.at += 1;
3218 left = Expr::Bin(Box::new(left), op, Box::new(self.term()?));
3219 }
3220 }
3221
3222 fn term(&mut self) -> R<Expr> {
3223 let mut left = self.power()?;
3224 loop {
3225 let op = match self.peek() {
3226 Some(Tok::Star) => BinOp::Mul,
3227 Some(Tok::Slash) => BinOp::Div,
3228 _ => return Ok(left),
3229 };
3230 self.at += 1;
3231 left = Expr::Bin(Box::new(left), op, Box::new(self.power()?));
3232 }
3233 }
3234
3235 fn power(&mut self) -> R<Expr> {
3236 let mut left = self.unary()?;
3237 while self.accept(&Tok::Power) {
3238 left = Expr::Bin(Box::new(left), BinOp::Pow, Box::new(self.unary()?));
3239 }
3240 Ok(left)
3241 }
3242
3243 fn unary(&mut self) -> R<Expr> {
3244 if self.accept(&Tok::Minus) {
3245 return Ok(Expr::Neg(Box::new(self.unary()?)));
3246 }
3247 self.accept(&Tok::Plus);
3248 if self.accept(&Tok::LParen) {
3249 let e = self.expr()?;
3250 self.expect(&Tok::RParen, "')'")?;
3251 return Ok(e);
3252 }
3253 Ok(Expr::Operand(self.operand()?))
3254 }
3255
3256 fn cond(&mut self) -> R<Cond> {
3257 let mut last = None;
3258 self.or_cond(&mut last)
3259 }
3260
3261 fn or_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
3262 let mut left = self.and_cond(last)?;
3263 while self.accept_word("OR") {
3264 left = Cond::Or(Box::new(left), Box::new(self.and_cond(last)?));
3265 }
3266 Ok(left)
3267 }
3268
3269 fn and_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
3270 let mut left = self.not_cond(last)?;
3271 while self.accept_word("AND") {
3272 left = Cond::And(Box::new(left), Box::new(self.not_cond(last)?));
3273 }
3274 Ok(left)
3275 }
3276
3277 fn not_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
3278 if self.is_word("NOT") && !self.relop_ahead(1) {
3279 self.at += 1;
3280 return Ok(Cond::Not(Box::new(self.not_cond(last)?)));
3281 }
3282 self.primary_cond(last)
3283 }
3284
3285 fn relop_ahead(&self, ahead: usize) -> bool {
3286 matches!(self.peek_at(ahead), Some(Tok::Eq | Tok::Lt | Tok::Gt | Tok::Le | Tok::Ge))
3287 || matches!(self.word_at(ahead), Some("EQUAL" | "GREATER" | "LESS"))
3288 }
3289
3290 fn primary_cond(&mut self, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
3291 let is = usize::from(self.is_word("IS"));
3292 if let Some((subject, ..)) = last.clone()
3293 && (self.relop_ahead(is) || self.word_at(is) == Some("NOT") && self.relop_ahead(is + 1))
3294 {
3295 self.accept_word("IS");
3296 let negated = self.accept_word("NOT");
3297 let op = self.relop()?.ok_or_else(|| self.error("a relational operator"))?;
3298 return self.objects(subject, op, negated, last);
3299 }
3300 if self.peek() == Some(&Tok::LParen) {
3301 let save = self.at;
3302 self.at += 1;
3303 let mut inner_last = last.clone();
3306 if let Ok(c) = self.or_cond(&mut inner_last)
3307 && self.accept(&Tok::RParen)
3308 && !matches!(self.peek(), Some(Tok::Plus | Tok::Minus | Tok::Star | Tok::Slash | Tok::Power))
3309 && !self.relop_ahead(0)
3310 && !self.is_word("IS")
3311 {
3312 return Ok(c);
3313 }
3314 self.at = save;
3315 }
3316 let start = self.at;
3317 let left = self.expr()?;
3318 self.accept_word("IS");
3319 let negated = self.is_word("NOT") && {
3320 self.at += 1;
3321 true
3322 };
3323 let wrap = |c: Cond| if negated { Cond::Not(Box::new(c)) } else { c };
3324 if let Some(op) = self.relop()? {
3325 return self.objects(left, op, negated, last);
3326 }
3327 if let Some(name) = self.word().filter(|w| self.classes.iter().any(|c| c.name == *w)).map(str::to_owned) {
3328 self.at += 1;
3329 return Ok(wrap(Cond::Class(left, Class::Named(name))));
3330 }
3331 if let Some(class) = self.accept_any(&["NUMERIC", "ALPHABETIC", "ALPHABETIC-LOWER", "ALPHABETIC-UPPER", "DBCS", "KANJI", "POSITIVE", "NEGATIVE", "ZERO"]) {
3332 let class = match class.as_str() {
3333 "NUMERIC" => Class::Numeric,
3334 "ALPHABETIC" => Class::Alphabetic,
3335 "ALPHABETIC-LOWER" => Class::AlphabeticLower,
3336 "ALPHABETIC-UPPER" => Class::AlphabeticUpper,
3337 "DBCS" => Class::Dbcs,
3338 "KANJI" => Class::Kanji,
3339 "POSITIVE" => Class::Positive,
3340 "NEGATIVE" => Class::Negative,
3341 _ => Class::Zero,
3342 };
3343 return Ok(wrap(Cond::Class(left, class)));
3344 }
3345 if negated {
3346 return Err(self.error("a relational operator or class after NOT"));
3347 }
3348 match (left, last.clone()) {
3349 (Expr::Operand(Operand::Ref(name)), Some((subject, op, negated))) if self.abbreviation_context(start) => Ok(Cond::NameOrRel { subject, op, negated, name }),
3350 (right, Some((subject, op, negated))) if !matches!(&right, Expr::Operand(Operand::Ref(_))) => Ok(relation(subject, op, negated, right)),
3351 (Expr::Operand(Operand::Ref(r)), _) => Ok(Cond::Name(r)),
3352 (_, _) => Err(self.error("a relational operator")),
3353 }
3354 }
3355
3356 fn objects(&mut self, subject: Expr, op: RelOp, negated: bool, last: &mut Option<(Expr, RelOp, bool)>) -> R<Cond> {
3359 *last = Some((subject.clone(), op, negated));
3360 if self.peek() == Some(&Tok::LParen) && self.word_at(1) == Some("NOT") && !self.relop_ahead(2) {
3361 return Err(crate::messages::IWS0064.at(self.pos(), "NOT cannot follow the left parenthesis that distributes a relational operator"));
3362 }
3363 let start = self.at;
3364 match self.distributed(&subject, op, negated) {
3365 Ok(Some(c)) => return Ok(c),
3366 _ => self.at = start,
3367 }
3368 let object = self.expr()?;
3369 Ok(relation(subject, op, negated, object))
3370 }
3371
3372 fn distributed(&mut self, subject: &Expr, op: RelOp, negated: bool) -> R<Option<Cond>> {
3375 let start = self.at;
3376 if !self.accept(&Tok::LParen) {
3377 return Ok(None);
3378 }
3379 let mut logical = false;
3380 let mut any = self.distributed_all(subject, op, negated, &mut logical)?;
3381 while self.accept_word("OR") {
3382 logical = true;
3383 any = Cond::Or(Box::new(any), Box::new(self.distributed_all(subject, op, negated, &mut logical)?));
3384 }
3385 if !logical || !self.accept(&Tok::RParen) {
3386 self.at = start;
3387 return Ok(None);
3388 }
3389 Ok(Some(any))
3390 }
3391
3392 fn distributed_all(&mut self, subject: &Expr, op: RelOp, negated: bool, logical: &mut bool) -> R<Cond> {
3393 let mut all = self.distributed_object(subject, op, negated)?;
3394 while self.accept_word("AND") {
3395 *logical = true;
3396 all = Cond::And(Box::new(all), Box::new(self.distributed_object(subject, op, negated)?));
3397 }
3398 Ok(all)
3399 }
3400
3401 fn distributed_object(&mut self, subject: &Expr, op: RelOp, negated: bool) -> R<Cond> {
3402 if self.accept_word("NOT") {
3403 return Ok(Cond::Not(Box::new(self.distributed_object(subject, op, negated)?)));
3404 }
3405 let start = self.at;
3406 match self.distributed(subject, op, negated) {
3407 Ok(Some(group)) => return Ok(group),
3408 _ => self.at = start,
3409 }
3410 let object = self.expr()?;
3411 Ok(relation(subject.clone(), op, negated, object))
3412 }
3413
3414 fn abbreviation_context(&self, start: usize) -> bool {
3417 for t in self.tokens[..start].iter().rev() {
3418 match &t.tok {
3419 Tok::LParen => {}
3420 Tok::Word(w) if w == "NOT" => {}
3421 Tok::Word(w) => return w == "OR" || w == "AND",
3422 _ => return false,
3423 }
3424 }
3425 false
3426 }
3427
3428 fn relop(&mut self) -> R<Option<RelOp>> {
3429 if self.peek() == Some(&Tok::Lt) && self.peek_at(1) == Some(&Tok::Gt) {
3430 return Err(crate::messages::IWS0065.at(self.pos(), "<> is not an Enterprise COBOL relational operator: it writes NOT ="));
3431 }
3432 let op = match self.peek() {
3433 Some(Tok::Eq) => RelOp::Eq,
3434 Some(Tok::Lt) => RelOp::Lt,
3435 Some(Tok::Gt) => RelOp::Gt,
3436 Some(Tok::Le) => RelOp::Le,
3437 Some(Tok::Ge) => RelOp::Ge,
3438 Some(Tok::Word(w)) if w == "EQUAL" => {
3439 self.at += 1;
3440 self.accept_word("TO");
3441 return Ok(Some(RelOp::Eq));
3442 }
3443 Some(Tok::Word(w)) if w == "GREATER" || w == "LESS" => {
3444 let greater = w == "GREATER";
3445 self.at += 1;
3446 self.accept_word("THAN");
3447 let or_equal = self.accept_word("OR");
3448 if or_equal {
3449 self.expect_word("EQUAL")?;
3450 self.accept_word("TO");
3451 }
3452 return Ok(Some(match (greater, or_equal) {
3453 (true, false) => RelOp::Gt,
3454 (true, true) => RelOp::Ge,
3455 (false, false) => RelOp::Lt,
3456 (false, true) => RelOp::Le,
3457 }));
3458 }
3459 _ => return Ok(None),
3460 };
3461 self.at += 1;
3462 Ok(Some(op))
3463 }
3464}
3465
3466fn host_variables(sql: &str, pos: Pos) -> Vec<Ref> {
3468 let chars: Vec<char> = sql.chars().collect();
3469 let (mut out, mut i, mut quote) = (Vec::new(), 0, None);
3470 while i < chars.len() {
3471 let c = chars[i];
3472 match quote {
3473 Some(q) if c == q => quote = None,
3474 Some(_) => {}
3475 None if c == '\'' || c == '"' => quote = Some(c),
3476 None if c == ':' && chars.get(i + 1).is_some_and(|n| n.is_ascii_alphanumeric()) => {
3477 let start = i + 1;
3478 let mut end = start;
3479 while end < chars.len() && (chars[end].is_ascii_alphanumeric() || matches!(chars[end], '-' | '_' | '.')) {
3480 end += 1;
3481 }
3482 let path: String = chars[start..end].iter().collect::<String>().to_ascii_uppercase();
3483 let mut parts: Vec<String> = path.trim_end_matches('.').split('.').map(str::to_owned).collect();
3484 let name = parts.pop().unwrap_or_default();
3485 parts.reverse();
3486 out.push(Ref { name, qualifiers: parts, subscripts: Vec::new(), refmod: None, pos });
3487 i = end;
3488 continue;
3489 }
3490 None => {}
3491 }
3492 i += 1;
3493 }
3494 out
3495}
3496
3497fn cics_options(body: &str) -> Vec<(String, Option<ExecArg>)> {
3499 let chars: Vec<char> = body.chars().collect();
3500 let (mut out, mut i) = (Vec::new(), 0);
3501 while i < chars.len() {
3502 if chars[i].is_whitespace() {
3503 i += 1;
3504 continue;
3505 }
3506 let start = i;
3507 while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '(' {
3508 i += 1;
3509 }
3510 let name: String = chars[start..i].iter().collect::<String>().to_ascii_uppercase();
3511 let mut j = i;
3512 while j < chars.len() && chars[j].is_whitespace() {
3513 j += 1;
3514 }
3515 if j < chars.len() && chars[j] == '(' {
3516 let (mut depth, mut quote, mut k) = (0, None, j);
3517 while k < chars.len() {
3518 match (quote, chars[k]) {
3519 (Some(q), c) if c == q => quote = None,
3520 (Some(_), _) => {}
3521 (None, '\'' | '"') => quote = Some(chars[k]),
3522 (None, '(') => depth += 1,
3523 (None, ')') => {
3524 depth -= 1;
3525 if depth == 0 {
3526 break;
3527 }
3528 }
3529 _ => {}
3530 }
3531 k += 1;
3532 }
3533 let arg: String = chars[j + 1..k.min(chars.len())].iter().collect();
3534 out.push((name, Some(ExecArg::Text(arg.trim().to_owned()))));
3535 i = (k + 1).min(chars.len());
3536 } else {
3537 out.push((name, None));
3538 }
3539 }
3540 out
3541}
3542
3543fn operand_of(text: &str, pos: Pos) -> Option<Operand> {
3545 let source = crate::source::Source { text: text.to_owned(), positions: vec![pos; text.chars().count()], options: Vec::new(), debugging: None, free: Vec::new() };
3546 let tokens = crate::lexer::lex(&source).ok()?;
3547 let mut p = Parser::new(&tokens);
3548 let op = p.operand().ok()?;
3549 (p.at == tokens.len()).then_some(op)
3550}
3551
3552fn system_entries(member: &str) -> R<Vec<DataEntry>> {
3554 system_text_entries(&crate::system::member(member).unwrap_or_default())
3555}
3556
3557fn system_text_entries(text: &str) -> R<Vec<DataEntry>> {
3558 let source = crate::source::read(text)?;
3559 let tokens = crate::lexer::lex(&source)?;
3560 Parser::new(&tokens).data_entries()
3561}
3562
3563const SELECT_CLAUSES: &[&str] = &[
3565 "ASSIGN", "ORGANIZATION", "ACCESS", "FILE", "STATUS", "RECORD", "ALTERNATE", "RELATIVE", "LINE", "SEQUENTIAL", "INDEXED", "RESERVE",
3566 "PADDING", "LOCK", "SHARING", "PASSWORD",
3567];
3568
3569const FD_WORDS: &[&str] = &[
3571 "RECORDING", "RECORD", "BLOCK", "LABEL", "DATA", "VALUE", "CODE-SET", "LINAGE", "REPORT", "REPORTS", "IS", "EXTERNAL", "GLOBAL", "STYLE",
3572];
3573
3574fn is_clause_word(w: &str) -> bool {
3575 matches!(
3576 w,
3577 "PIC" | "PICTURE" | "USAGE" | "VALUE" | "VALUES" | "REDEFINES" | "OCCURS" | "SIGN" | "LEADING" | "TRAILING" | "JUSTIFIED"
3578 | "JUST" | "SYNC" | "SYNCHRONIZED" | "GLOBAL" | "EXTERNAL" | "BLANK"
3579 ) || usage_word(w).is_some()
3580}
3581
3582#[cfg(test)]
3583mod tests {
3584 use super::*;
3585
3586 fn program(body: &str) -> Program {
3587 let text = format!(
3588 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n{body}"
3589 );
3590 crate::parse(&text).unwrap_or_else(|e| panic!("{e}"))
3591 }
3592
3593 #[test]
3594 fn dfhresp_and_dfhvalue_fold_to_the_numbers_in_ibms_tables() {
3595 let body = |operand: &str| format!(" 01 A PIC S9(8) COMP.\n PROCEDURE DIVISION.\n IF A = {operand}\n GOBACK\n END-IF.\n");
3596 let folds_to = |operand: &str, n: i32| {
3597 let p = program(&body(operand));
3598 assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains(&format!("Number(\"{n}\")")), "{operand}");
3599 };
3600 folds_to("DFHRESP(NOTFINISHED)", 113);
3601 folds_to("DFHRESP(DSIDERR)", 12);
3602 folds_to("DFHRESP(FILENOTFOUND)", 12);
3603 assert_eq!(rt::cics_tables::cvda("ENABLED"), Some(23));
3604 folds_to("DFHVALUE(ENABLED)", 23);
3605 for (operand, message) in [("DFHRESP(NOSUCH)", "DFHRESP(NOSUCH): not a CICS condition"), ("DFHVALUE(NOSUCH)", "DFHVALUE(NOSUCH): not a CVDA")] {
3606 let err = crate::parse(&format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n{}", body(operand))).unwrap_err();
3607 assert!(err.to_string().contains(message), "{err}");
3608 }
3609 }
3610
3611 #[test]
3612 fn an_exec_cics_program_compares_with_dfhresp_notfinished() {
3613 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");
3614 }
3615
3616 #[test]
3617 fn data_entries_with_clauses_in_any_order() {
3618 let p = program(
3619 " 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",
3620 );
3621 let ws = &p.working_storage;
3622 assert_eq!(ws.len(), 5);
3623 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()))));
3624 assert_eq!(ws[2].redefines.as_deref(), Some("A"));
3625 assert_eq!((ws[3].name.as_deref(), &ws[3].value), (None, &Some(Literal::Figurative(Figurative::Space))));
3626 assert_eq!(ws[4].occurs, Some(3));
3627 }
3628
3629 #[test]
3630 fn external_and_global_are_kept_and_a_contained_program_sees_its_containers_global_records() {
3631 let text = [
3632 " IDENTIFICATION DIVISION.\n PROGRAM-ID. A.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
3633 " SELECT F ASSIGN TO FDD.\n SELECT H ASSIGN TO HDD.\n DATA DIVISION.\n FILE SECTION.\n",
3634 " FD F IS GLOBAL IS EXTERNAL.\n 01 F-REC PIC X.\n FD H.\n 01 H-REC PIC X.\n",
3635 " 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",
3636 " PROCEDURE DIVISION.\n GOBACK.\n",
3637 " 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",
3638 " IDENTIFICATION DIVISION.\n PROGRAM-ID. C.\n PROCEDURE DIVISION.\n GOBACK.\n",
3639 " END PROGRAM C.\n END PROGRAM B.\n END PROGRAM A.\n",
3640 ]
3641 .concat();
3642 let all = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
3643 let a = &all[0];
3644 assert!(a.working_storage[0].external && !a.working_storage[0].global && a.working_storage[1].global && !a.working_storage[2].global);
3645 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);
3646 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<_>>();
3647 assert_eq!(containers(&all[1]), [("A".to_owned(), vec!["G".to_owned(), "G1".to_owned()], 1)]);
3648 assert_eq!(containers(&all[2]), [("B".to_owned(), vec!["BG".to_owned()], 0), ("A".to_owned(), vec!["G".to_owned(), "G1".to_owned()], 1)]);
3649 let on_record = text.replace(" 01 F-REC PIC X.", " 01 F-REC PIC X EXTERNAL.");
3650 let err = crate::parse_all_with(&on_record, &crate::copy::Libraries::default()).unwrap_err();
3651 assert!(err.message.contains("FD F: EXTERNAL goes on the FD, not on a record of the FILE SECTION"), "{err}");
3652 }
3653
3654 #[test]
3655 fn paragraphs_and_nested_if() {
3656 let p = program(
3657 " 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",
3658 );
3659 assert_eq!(p.paragraphs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["MAIN-LINE", "P2"]);
3660 let Stmt::If { otherwise, .. } = &p.paragraphs[1].statements[0] else { panic!() };
3661 assert!(matches!(otherwise[0], Stmt::If { .. }));
3662 }
3663
3664 #[test]
3665 fn a_program_names_the_programs_it_directly_contains() {
3666 let program = |id: &str, inner: &str| format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. {id}.\n PROCEDURE DIVISION.\n GOBACK.\n{inner} END PROGRAM {id}.\n");
3667 let text = program("OUTER", &[program("A", &program("A1", "")), program("B", "")].concat());
3668 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
3669 let contained: Vec<(&str, &[String])> = all.iter().map(|p| (p.id.as_str(), p.nested.as_slice())).collect();
3670 assert_eq!(contained, [("OUTER", &["A".to_owned(), "B".to_owned()][..]), ("A", &["A1".to_owned()][..]), ("A1", &[][..]), ("B", &[][..])]);
3671 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");
3672 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
3673 let common: Vec<(&str, bool, bool)> = all.iter().map(|p| (p.id.as_str(), p.common, p.initial)).collect();
3674 assert_eq!(common, [("OUTER", false, false), ("A", true, false), ("A1", true, true), ("B", false, false)]);
3675 }
3676
3677 #[test]
3678 fn each_program_carries_the_messages_of_its_own_source_and_not_its_contained_programs() {
3679 let program = |id: &str, name: &str, inner: &str| {
3680 format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. {id}.\n PROCEDURE DIVISION.\n DISPLAY {name}\n GOBACK.\n{inner} END PROGRAM {id}.\n")
3681 };
3682 let text = [program("O#1", "O1", &[program("A", "A@1", ""), program("B", "B1", "")].concat()), program("NEXT", "N%1", "")].concat();
3683 let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
3684 let lines: Vec<(&str, Vec<u32>)> = all.iter().map(|p| (p.id.as_str(), p.messages.iter().map(|m| m.pos.line).collect())).collect();
3685 assert_eq!(lines, [("O#1", vec![2, 18]), ("A", vec![9]), ("B", vec![]), ("NEXT", vec![22])]);
3686 }
3687
3688 #[test]
3689 fn inline_perform_varying_with_a_compound_condition() {
3690 let p = program(
3691 " 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",
3692 );
3693 let Stmt::PerformInline { repeat: Loop::Varying { varying, .. }, .. } = &p.paragraphs[0].statements[0] else { panic!() };
3694 assert!(matches!(varying.until, Cond::Or(..)));
3695 }
3696
3697 #[test]
3698 fn functions_reference_modification_and_length_of() {
3699 let p = program(
3700 " 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",
3701 );
3702 let Stmt::Compute { expr: Expr::Bin(left, BinOp::Sub, _), .. } = &p.paragraphs[0].statements[0] else { panic!() };
3703 let Expr::Operand(Operand::Function(f)) = left.as_ref() else { panic!() };
3704 let Expr::Operand(Operand::Ref(r)) = &f.args[0] else { panic!() };
3705 assert!(r.refmod.is_some());
3706 assert!(matches!(&p.paragraphs[0].statements[2], Stmt::Move { from: Operand::LengthOf(_), .. }));
3707 }
3708
3709 #[test]
3710 fn divide_giving_remainder_and_add_to() {
3711 let p = program(
3712 " 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",
3713 );
3714 let Stmt::Arith(a) = &p.paragraphs[0].statements[0] else { panic!() };
3715 assert_eq!(a.verb, ArithVerb::Divide);
3716 assert!(a.remainder.is_some());
3717 let Stmt::Arith(add) = &p.paragraphs[0].statements[1] else { panic!() };
3718 assert!(add.computations[0].0.rounded);
3719 }
3720
3721 #[test]
3722 fn abbreviated_combined_relation() {
3723 let p = program(" 01 A PIC 9.\n PROCEDURE DIVISION.\n IF A = 1 OR 2 CONTINUE END-IF.\n");
3724 let Stmt::If { cond: Cond::Or(_, right), .. } = &p.paragraphs[0].statements[0] else { panic!() };
3725 assert!(matches!(right.as_ref(), Cond::Rel(_, RelOp::Eq, _)));
3726 }
3727
3728 #[test]
3729 fn indexed_select_clauses_and_keyed_statements() {
3730 let text = [
3731 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
3732 " SELECT K ASSIGN TO KDD ORGANIZATION IS INDEXED\n",
3733 " ACCESS MODE IS DYNAMIC RECORD KEY IS K-ID\n",
3734 " ALTERNATE KEY K-ALT WITH DUPLICATES.\n",
3735 " SELECT N ASSIGN TO NDD STATUS N-FS N-VS ORGANIZATION\n",
3736 " INDEXED FILE STATUS IS N-FS RECORD N-ID.\n",
3737 " 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",
3738 " 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",
3739 " PROCEDURE DIVISION.\n",
3740 " READ K NEXT RECORD AT END CONTINUE\n",
3741 " NOT AT END CONTINUE END-READ\n",
3742 " READ K KEY IS K-ALT INVALID KEY CONTINUE END-READ\n",
3743 " START K KEY IS NOT LESS THAN K-ID\n",
3744 " INVALID KEY CONTINUE NOT INVALID KEY CONTINUE\n",
3745 " END-START\n",
3746 " REWRITE K-REC INVALID KEY CONTINUE END-REWRITE\n",
3747 " DELETE K RECORD END-DELETE.\n",
3748 ]
3749 .concat();
3750 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
3751 let f = &p.files[0];
3752 assert_eq!((f.organization, f.access), (Organization::Indexed, Access::Dynamic));
3753 assert_eq!(f.record_key.as_ref().map(|r| r.name.as_str()), Some("K-ID"));
3754 assert_eq!(f.alternate_keys.iter().map(|(r, d)| (r.name.as_str(), *d)).collect::<Vec<_>>(), [("K-ALT", true)]);
3755 let n = &p.files[1];
3756 assert_eq!((n.organization, n.record_key.as_ref().map(|r| r.name.as_str())), (Organization::Indexed, Some("N-ID")));
3757 let s = &p.paragraphs[0].statements;
3758 let Stmt::Read(r) = &s[0] else { panic!() };
3759 assert!(r.next && !r.previous && r.at_end.on.is_some() && r.at_end.not_on.is_some());
3760 let Stmt::Read(r) = &s[1] else { panic!() };
3761 assert!(r.key.is_some() && r.invalid.on.is_some() && !r.next);
3762 let Stmt::Start { key: Some((RelOp::Ge, _)), invalid, .. } = &s[2] else { panic!() };
3763 assert!(invalid.on.is_some() && invalid.not_on.is_some());
3764 assert!(matches!(&s[3], Stmt::Rewrite { invalid, .. } if invalid.on.is_some()));
3765 assert!(matches!(&s[4], Stmt::Delete { .. }));
3766 }
3767
3768 #[test]
3769 fn entry_alter_the_go_to_forms_and_section_priorities() {
3770 let p = program(
3771 " 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",
3772 );
3773 let priorities: Vec<(&str, u8)> = p.paragraphs.iter().map(|q| (q.name.as_str(), q.priority)).collect();
3774 assert_eq!(priorities, [("S", 50), ("P1", 50), ("P2", 50), ("P3", 50), ("T", 0)]);
3775 let s = &p.paragraphs[1].statements;
3776 assert!(matches!(&s[0], Stmt::Entry { name, using, .. } if name == "ALT" && using == &[Param { by_value: true, name: "L".into() }]));
3777 assert!(matches!(&s[2], Stmt::Alter { pairs, .. } if pairs.len() == 2 && pairs[1].0.name == "P3"));
3778 assert!(matches!(&s[3], Stmt::GoToDepending { targets, on, .. } if targets.len() == 2 && on.name == "D"));
3779 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::GoTo { target: None, .. }));
3780 assert!(matches!(&p.paragraphs[3].statements[0], Stmt::GoTo { target: Some(t), .. } if t.name == "P1"));
3781 }
3782
3783 #[test]
3784 fn by_left_out_stop_literal_and_a_phrase_that_belongs_to_the_outer_statement() {
3785 let p = program(
3786 " 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",
3787 );
3788 let s = &p.paragraphs[0].statements;
3789 let Stmt::Call(c) = &s[0] else { panic!("{:?}", s[0]) };
3790 let modes: Vec<ArgMode> = c.using.iter().map(|a| a.mode).collect();
3791 assert_eq!(modes, [ArgMode::Content, ArgMode::Reference, ArgMode::Value, ArgMode::Value]);
3792 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());
3793 assert!(matches!(&s[1], Stmt::Display { items, .. } if items == &[Operand::Literal(Literal::Alnum("OPERATOR".into()))]));
3794 assert!(matches!(&s[2], Stmt::Display { .. }) && matches!(&s[3], Stmt::StopRun { .. }));
3795 }
3796
3797 #[test]
3798 fn select_clauses_with_their_optional_words_left_out() {
3799 let text = [
3800 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
3801 " SELECT R ASSIGN RDD ORGANIZATION RELATIVE ACCESS RANDOM\n RELATIVE R-KEY.\n",
3802 " SELECT Q ASSIGN QDD RELATIVE ACCESS DYNAMIC\n RELATIVE IS Q-KEY.\n",
3803 " SELECT X ASSIGN XDD ORGANIZATION INDEXED\n RECORD X-KEY ALTERNATE RECORD X-ALT\n ALTERNATE RECORD IS X-ALT2 WITH DUPLICATES.\n",
3804 " SELECT S ASSIGN SDD RECORD DELIMITER IS STANDARD-1.\n",
3805 " DATA DIVISION.\n FILE SECTION.\n FD R.\n 01 R-REC PIC X.\n FD Q.\n 01 Q-REC PIC X.\n",
3806 " 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",
3807 " WORKING-STORAGE SECTION.\n 01 R-KEY PIC 9.\n 01 Q-KEY PIC 9.\n PROCEDURE DIVISION.\n GOBACK.\n",
3808 ]
3809 .concat();
3810 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
3811 let f = &p.files;
3812 assert!(f[0].organization == Organization::Relative && f[0].relative_key.as_ref().is_some_and(|k| k.name == "R-KEY"));
3813 assert!(f[1].organization == Organization::Relative && f[1].relative_key.as_ref().is_some_and(|k| k.name == "Q-KEY"));
3814 let alternates: Vec<(&str, bool)> = f[2].alternate_keys.iter().map(|(k, d)| (k.name.as_str(), *d)).collect();
3815 assert_eq!(alternates, [("X-ALT", false), ("X-ALT2", true)]);
3816 assert!(f[3].record_key.is_none() && f[3].organization == Organization::Sequential);
3817 }
3818
3819 #[test]
3820 fn assign_keeps_its_first_name_and_password_is_read() {
3821 let text = [
3822 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
3823 " SELECT F ASSIGN TO UT-S-FDD XXXXX044 'B.DAT'\n ORGANIZATION SEQUENTIAL.\n",
3824 " SELECT K ASSIGN KDD ORGANIZATION INDEXED\n RECORD KEY IS KK PASSWORD IS PW\n ALTERNATE RECORD KA PASSWORD PW.\n",
3825 " 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",
3826 " WORKING-STORAGE SECTION.\n 01 PW PIC X(8).\n PROCEDURE DIVISION.\n GOBACK.\n",
3827 ]
3828 .concat();
3829 let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
3830 assert_eq!((p.files[0].assign.as_str(), p.files[0].organization), ("FDD", Organization::Sequential));
3831 assert_eq!(p.files[1].alternate_keys.iter().map(|(k, _)| k.name.as_str()).collect::<Vec<_>>(), ["KA"]);
3832 }
3833
3834 #[test]
3835 fn procedure_names_of_digits_and_a_subscripted_times_count() {
3836 let p = program(
3837 " 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",
3838 );
3839 let names: Vec<&str> = p.paragraphs.iter().map(|q| q.name.as_str()).collect();
3840 assert_eq!(names, ["00", "10", "20", "30", "40"]);
3841 let s = &p.paragraphs[1].statements;
3842 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));
3843 assert!(matches!(&s[1], Stmt::PerformProc { from, thru: Some(t), repeat: Loop::Once, .. } if from.name == "20" && t.name == "30"));
3844 assert!(matches!(&s[2], Stmt::PerformInline { repeat: Loop::Times(_), .. }));
3845 assert!(matches!(&s[3], Stmt::PerformInline { repeat: Loop::Until { test_after: false, .. }, .. }));
3846 assert!(matches!(&s[4], Stmt::GoToDepending { targets, .. } if targets.len() == 2 && targets[1].name == "30"));
3847 assert!(matches!(&s[5], Stmt::Alter { pairs, .. } if pairs.len() == 2 && pairs[1].0.name == "40"));
3848 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::PerformProc { from, .. } if from.name == "00"));
3849 }
3850
3851 #[test]
3852 fn corresponding_forms_take_one_receiving_group() {
3853 let p = program(
3854 " 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",
3855 );
3856 let s = &p.paragraphs[0].statements;
3857 let Stmt::Corresponding(m) = &s[0] else { panic!("{:?}", s[0]) };
3858 assert_eq!((m.verb, m.from.name.as_str(), m.to.name.as_str(), m.to.subscripts.len()), (CorrespondingVerb::Move, "A", "B", 1));
3859 let Stmt::Corresponding(a) = &s[1] else { panic!() };
3860 assert!(a.verb == CorrespondingVerb::Add && a.rounded && a.size_error.is_some());
3861 assert!(matches!(&s[2], Stmt::Corresponding(c) if c.verb == CorrespondingVerb::Subtract && !c.rounded));
3862 let text = " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n PROCEDURE DIVISION.\n MOVE CORRESPONDING A TO B C.\n";
3863 assert!(crate::parse(text).unwrap_err().message.contains("one receiving group"));
3864 }
3865
3866 #[test]
3867 fn upsi_switch_entries_take_each_form_and_reach_contained_programs() {
3868 let text = [
3869 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
3870 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n UPSI-0 IS ABBREV-SWITCH\n",
3871 " ON ON-SWITCH OFF IS OFF-SWITCH\n UPSI-1 OFF STATUS IS F1 ON T1\n",
3872 " UPSI-7 SW-7 C01 IS TOP.\n PROCEDURE DIVISION.\n",
3873 " SET ABBREV-SWITCH SW-7 TO ON SW-7 TO OFF.\n",
3874 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n GOBACK.\n",
3875 " END PROGRAM INNER.\n END PROGRAM OUTER.\n",
3876 ]
3877 .concat();
3878 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
3879 let (outer, inner) = (&programs[0], &programs[1]);
3880 let entry = |p: &Program, k: usize| {
3881 let s = &p.environment.switches[k];
3882 format!("{} {:?} {:?} {:?}", s.number, s.mnemonic, s.on, s.off)
3883 };
3884 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"#];
3885 assert_eq!((0..3).map(|k| entry(outer, k)).collect::<Vec<_>>(), expected);
3886 assert_eq!((0..3).map(|k| entry(inner, k)).collect::<Vec<_>>(), expected);
3887 assert_eq!(outer.environment.mnemonics, [("TOP".to_owned(), "C01".to_owned())]);
3888 let Stmt::Set { set: SetStmt::Switches(groups), .. } = &outer.paragraphs[0].statements[0] else { panic!("{:?}", outer.paragraphs[0].statements) };
3889 let names: Vec<(Vec<&str>, bool)> = groups.iter().map(|(t, on)| (t.iter().map(|r| r.name.as_str()).collect(), *on)).collect();
3890 assert_eq!(names, [(vec!["ABBREV-SWITCH", "SW-7"], true), (vec!["SW-7"], false)]);
3891 }
3892
3893 #[test]
3894 fn an_upsi_switch_entry_names_something_and_each_status_once() {
3895 let entry = |clause: &str| {
3896 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");
3897 crate::parse(&text).err().map(|e| e.message)
3898 };
3899 assert_eq!(entry("UPSI-2"), Some("UPSI-2: a mnemonic-name or an ON or OFF STATUS phrase must follow it".into()));
3900 assert_eq!(entry("UPSI-2 ON A ON B"), Some("UPSI-2: a second ON STATUS phrase".into()));
3901 assert_eq!(entry("UPSI-2 IS S OFF A"), None);
3902 }
3903
3904 #[test]
3905 fn advancing_mnemonic_names_reach_contained_programs_and_linage_is_noted() {
3906 let text = [
3907 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
3908 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n C01 IS TOP-OF-PAGE CSP NO-SPACE\n",
3909 " AFP-5A IS PAGE-MODE UPSI-0 IS SWITCH-0 ON STATUS IS SW-ON.\n",
3910 " INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n SELECT P ASSIGN TO PDD.\n",
3911 " DATA DIVISION.\n FILE SECTION.\n FD P LINAGE IS 60.\n 01 P-REC PIC X.\n",
3912 " PROCEDURE DIVISION.\n WRITE P-REC AFTER TOP-OF-PAGE\n",
3913 " WRITE P-REC BEFORE ADVANCING NO-SPACE\n WRITE P-REC AFTER ADVANCING PAGE-COUNT LINES.\n",
3914 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n",
3915 " WRITE P-REC AFTER ADVANCING PAGE-MODE.\n END PROGRAM INNER.\n END PROGRAM OUTER.\n",
3916 ]
3917 .concat();
3918 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
3919 let (outer, inner) = (&programs[0], &programs[1]);
3920 let pairs = |p: &Program| p.environment.mnemonics.iter().map(|(n, e)| format!("{n}={e}")).collect::<Vec<_>>();
3921 assert_eq!(pairs(outer), ["TOP-OF-PAGE=C01", "NO-SPACE=CSP", "PAGE-MODE=AFP-5A"]);
3922 assert_eq!(outer.files[0].linage.as_ref().map(|l| &l.lines), Some(&LinageValue::Integer("60".into())));
3923 let advancing = |p: &Program, i: usize| match &p.paragraphs[0].statements[i] {
3924 Stmt::Write { advancing: Some(a), .. } => a.clone(),
3925 other => panic!("{other:?}"),
3926 };
3927 assert!(matches!(advancing(outer, 0), Advancing::Mnemonic { before: false, environment, .. } if environment == "C01"));
3928 assert!(matches!(advancing(outer, 1), Advancing::Mnemonic { before: true, environment, .. } if environment == "CSP"));
3929 assert!(matches!(advancing(outer, 2), Advancing::Lines { before: false, .. }));
3930 assert!(matches!(advancing(inner, 0), Advancing::Mnemonic { environment, .. } if environment == "AFP-5A"));
3931 }
3932
3933 #[test]
3934 fn exit_section_opening_a_paragraph_is_a_statement_not_a_section() {
3935 let p = program(" PROCEDURE DIVISION.\n MAIN-LINE SECTION.\n SKIPPED.\n EXIT SECTION.\n NEVER.\n GOBACK.\n");
3936 let names: Vec<&str> = p.paragraphs.iter().map(|q| q.name.as_str()).collect();
3937 assert_eq!(names, ["MAIN-LINE", "SKIPPED", "NEVER"]);
3938 assert!(matches!(p.paragraphs[1].statements[..], [Stmt::Exit { kind: ExitKind::Section, .. }, Stmt::SentenceEnd]), "{:?}", p.paragraphs[1].statements);
3939 }
3940
3941 fn linage_program(fds: &str, procedure: &str) -> String {
3942 [
3943 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n",
3944 " SELECT P ASSIGN TO PDD.\n SELECT S ASSIGN TO SDD.\n SELECT Q ASSIGN TO QDD.\n",
3945 " DATA DIVISION.\n FILE SECTION.\n",
3946 fds,
3947 " WORKING-STORAGE SECTION.\n 01 SIZES.\n 05 BODY PIC 99.\n 01 T-M PIC 9.\n PROCEDURE DIVISION.\n",
3948 procedure,
3949 ]
3950 .concat()
3951 }
3952
3953 #[test]
3954 fn linage_phrases_come_in_any_order_and_an_sds_is_dropped() {
3955 let fds = [
3956 " 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",
3957 " SD S LINAGE 10.\n 01 S-REC PIC X.\n",
3958 " FD Q LABEL RECORDS STANDARD LINAGE 5 RECORDING MODE F.\n 01 Q-REC PIC X.\n",
3959 ]
3960 .concat();
3961 let p = crate::parse(&linage_program(&fds, " GOBACK.\n")).unwrap_or_else(|e| panic!("{e}"));
3962 let l = p.files[0].linage.as_ref().unwrap();
3963 assert!(matches!(&l.lines, LinageValue::Data(r) if r.name == "BODY" && r.qualifiers == ["SIZES"]));
3964 assert_eq!((&l.footing, &l.bottom), (&Some(LinageValue::Integer("45".into())), &Some(LinageValue::Integer("6".into()))));
3965 assert!(matches!(&l.top, Some(LinageValue::Data(r)) if r.name == "T-M"));
3966 assert_eq!(p.files[1].linage, None);
3967 assert_eq!((p.files[2].linage.as_ref().map(|l| &l.lines), p.files[2].recording), (Some(&LinageValue::Integer("5".into())), Some('F')));
3968 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")] {
3969 let text = linage_program(&format!(" FD P {fd}.\n 01 P-REC PIC X.\n"), " GOBACK.\n");
3970 let e = crate::parse(&text).unwrap_err();
3971 assert!(e.message.contains(expected), "{fd}: {}", e.message);
3972 }
3973 }
3974
3975 #[test]
3976 fn record_varying_keeps_from_and_to_apart_and_an_odo_table_its_fewest_occurrences() {
3977 let fds = [
3978 " FD P RECORD IS VARYING IN SIZE TO 80 CHARACTERS.\n 01 P-REC PIC X.\n",
3979 " 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",
3980 " 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",
3981 ]
3982 .concat();
3983 let p = crate::parse(&linage_program(&fds, " GOBACK.\n")).unwrap_or_else(|e| panic!("{e}"));
3984 let bounds = |k: usize| (p.files[k].record_varying, p.files[k].record_min, p.files[k].record_max);
3985 assert_eq!([bounds(0), bounds(1), bounds(2)], [(true, None, Some(80)), (true, Some(10), None), (false, Some(10), Some(80))]);
3986 assert_eq!((p.files[1].records[2].occurs, p.files[1].records[2].occurs_min), (Some(9), Some(1)));
3987 assert_eq!((p.files[2].records[1].occurs, p.files[2].records[1].occurs_min), (Some(9), Some(2)));
3988 }
3989
3990 #[test]
3991 fn end_of_page_phrases_leave_a_read_its_not_at_end() {
3992 let procedure = [
3993 " READ Q AT END WRITE P-REC\n",
3994 " NOT AT END WRITE P-REC AT EOP CONTINUE END-WRITE\n END-READ\n",
3995 " WRITE P-REC BEFORE ADVANCING 2 LINES END-OF-PAGE CONTINUE\n",
3996 " NOT AT END-OF-PAGE CONTINUE\n END-WRITE\n",
3997 " WRITE P-REC INVALID KEY CONTINUE NOT EOP CONTINUE.\n",
3998 ]
3999 .concat();
4000 let fds = " FD P LINAGE 5.\n 01 P-REC PIC X.\n FD Q.\n 01 Q-REC PIC X.\n";
4001 let p = crate::parse(&linage_program(fds, &procedure)).unwrap_or_else(|e| panic!("{e}"));
4002 let s = &p.paragraphs[0].statements;
4003 let Stmt::Read(r) = &s[0] else { panic!("{:?}", s[0]) };
4004 let (Some(on), Some(not_on)) = (&r.at_end.on, &r.at_end.not_on) else { panic!("{r:?}") };
4005 assert!(matches!(&on[0], Stmt::Write { end_of_page, .. } if *end_of_page == Handlers::default()));
4006 assert!(matches!(¬_on[0], Stmt::Write { end_of_page, .. } if end_of_page.on.is_some() && end_of_page.not_on.is_none()));
4007 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()));
4008 assert!(matches!(&s[2], Stmt::Write { invalid, end_of_page, .. } if invalid.on.is_some() && end_of_page.not_on.is_some()));
4009 }
4010
4011 #[test]
4012 fn decimal_point_is_comma_and_currency_signs_reach_contained_programs() {
4013 let text = [
4014 " IDENTIFICATION DIVISION.\n PROGRAM-ID. OUTER.\n ENVIRONMENT DIVISION.\n",
4015 " CONFIGURATION SECTION.\n SPECIAL-NAMES.\n CURRENCY SIGN IS 'W'\n",
4016 " CURRENCY 'EUR ' WITH PICTURE SYMBOL 'y'\n DECIMAL-POINT IS COMMA.\n",
4017 " DATA DIVISION.\n WORKING-STORAGE SECTION.\n 01 A PIC 9V9 VALUE 1,5.\n",
4018 " PROCEDURE DIVISION.\n GOBACK.\n",
4019 " IDENTIFICATION DIVISION.\n PROGRAM-ID. INNER.\n PROCEDURE DIVISION.\n GOBACK.\n",
4020 " END PROGRAM INNER.\n END PROGRAM OUTER.\n",
4021 ]
4022 .concat();
4023 let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
4024 assert!(programs.iter().all(|p| p.environment.decimal_point_comma));
4025 assert_eq!(programs[0].working_storage[0].value, Some(Literal::Number("1.5".into())));
4026 let signs = [CurrencySign { value: "W".into(), symbol: 'W', hex: None }, CurrencySign { value: "EUR ".into(), symbol: 'y', hex: None }];
4027 assert!(programs.iter().all(|p| p.environment.currency == signs));
4028 for (clause, why) in [
4029 ("'E'", "one character"),
4030 ("'EUR'", "one character"),
4031 ("'E9' PICTURE SYMBOL 'Y'", "a digit"),
4032 ("'EUR' PICTURE SYMBOL 'Z'", "PICTURE SYMBOL"),
4033 ("X'5B5B'", "X'5B5B' is not one character"),
4034 ("X''", "a nonempty alphanumeric literal"),
4035 ("'EUR' WITH 'Y'", "PICTURE SYMBOL after WITH"),
4036 ("'W'\n CURRENCY 'WON' PICTURE SYMBOL 'W'", "a second CURRENCY SIGN"),
4037 ] {
4038 let bad = text.replace("CURRENCY SIGN IS 'W'", &format!("CURRENCY SIGN IS {clause}"));
4039 let message = crate::parse(&bad).unwrap_err().message;
4040 assert!(message.contains(why), "{clause}: {message}");
4041 }
4042 let hex = text.replace("CURRENCY SIGN IS 'W'", "CURRENCY SIGN IS X'5B'\n CURRENCY X'9F' PICTURE SYMBOL 'Y'\n CURRENCY X'86'");
4043 let mut p = crate::parse(&hex).unwrap_or_else(|e| panic!("{e}"));
4044 let pending = |symbol: char, bytes: &[u8]| CurrencySign { value: String::new(), symbol, hex: Some(bytes.to_vec()) };
4045 assert_eq!(p.environment.currency[..3], [pending(HEX_SYMBOL, &[0x5B]), pending('Y', &[0x9F]), pending(HEX_SYMBOL, &[0x86])]);
4046 let page = |bytes: &[u8]| bytes.iter().map(|b| match b { 0x5B => '$', 0x9F => '€', _ => 'f' }).collect();
4047 decode_currency(&mut p.environment, page).unwrap();
4048 let decoded = |value: &str, symbol: char| CurrencySign { value: value.into(), symbol, hex: None };
4049 assert_eq!(p.environment.currency[..3], [decoded("$", '$'), decoded("€", 'Y'), decoded("f", 'f')]);
4050 }
4051
4052 #[test]
4053 fn a_picture_keeps_the_case_of_a_currency_symbol_and_picture_symbol_is_no_picture_string() {
4054 let text = concat!(
4055 " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n CONFIGURATION SECTION.\n",
4056 " SPECIAL-NAMES.\n CURRENCY SIGN 'CHF ' WITH PICTURE SYMBOL 'f'.\n DATA DIVISION.\n",
4057 " WORKING-STORAGE SECTION.\n 01 A PIC fff9v99.\n 01 B PIC zz9.\n",
4058 );
4059 let p = crate::parse(text).unwrap_or_else(|e| panic!("{e}"));
4060 assert_eq!(p.environment.currency, [CurrencySign { value: "CHF ".into(), symbol: 'f', hex: None }]);
4061 assert_eq!((p.working_storage[0].picture.as_deref(), p.working_storage[1].picture.as_deref()), (Some("fff9V99"), Some("ZZ9")));
4062 }
4063
4064 #[test]
4065 fn renames_and_condition_names_with_a_false_value() {
4066 let p = program(concat!(
4067 " 01 R.\n 05 A PIC X.\n 05 B PIC X.\n 88 B-ON VALUE 'Y' FALSE 'N'.\n",
4068 " 88 B-OFF VALUES 'N' 'X' WHEN SET TO FALSE IS SPACE.\n",
4069 " 66 AB RENAMES A THRU B.\n 66 BB RENAMES B OF R.\n",
4070 " PROCEDURE DIVISION.\n SET B-ON B-OFF TO FALSE.\n",
4071 ));
4072 let ws = &p.working_storage;
4073 assert_eq!((ws[3].false_value.as_ref(), ws[3].condition_values.len()), (Some(&Literal::Alnum("N".into())), 1));
4074 assert_eq!((ws[4].false_value.as_ref(), ws[4].condition_values.len()), (Some(&Literal::Figurative(Figurative::Space)), 2));
4075 let (first, last) = ws[5].renames.as_ref().unwrap();
4076 assert_eq!((ws[5].level, first.name.as_str(), last.as_ref().map(|r| r.name.as_str())), (66, "A", Some("B")));
4077 assert_eq!(ws[6].renames.as_ref().unwrap().0.qualifiers, ["R"]);
4078 assert!(matches!(&p.paragraphs[0].statements[0], Stmt::Set { set: SetStmt::ConditionFalse(t), .. } if t.len() == 2));
4079 }
4080
4081 #[test]
4082 fn a_procedure_name_of_digits_alone_is_read_where_a_procedure_name_goes() {
4083 let p = program(concat!(
4084 " 01 D PIC 9.\n PROCEDURE DIVISION.\n 00 SECTION 50.\n 010.\n",
4085 " GO TO 3 010 OF 00 DEPENDING ON D\n PERFORM 3 TIMES DISPLAY D END-PERFORM\n",
4086 " PERFORM 3 THRU 4 2 TIMES\n DISPLAY 1\n 3.\n ALTER 4 TO 3.\n 4.\n GO TO 3.\n",
4087 ));
4088 let names: Vec<(&str, bool, u8)> = p.paragraphs.iter().map(|q| (q.name.as_str(), q.is_section, q.priority)).collect();
4089 assert_eq!(names, [("00", true, 50), ("010", false, 50), ("3", false, 50), ("4", false, 50)]);
4090 let s = &p.paragraphs[1].statements;
4091 assert!(matches!(&s[0], Stmt::GoToDepending { targets, .. } if targets[0].name == "3" && targets[1].section.as_deref() == Some("00")));
4092 assert!(matches!(&s[1], Stmt::PerformInline { repeat: Loop::Times(_), .. }));
4093 assert!(matches!(&s[2], Stmt::PerformProc { from, thru: Some(t), repeat: Loop::Times(_), .. } if from.name == "3" && t.name == "4"));
4094 assert!(matches!(&s[3], Stmt::Display { items, .. } if items.len() == 1));
4095 assert!(matches!(&p.paragraphs[2].statements[0], Stmt::Alter { pairs, .. } if pairs[0].0.name == "4" && pairs[0].1.name == "3"));
4096 }
4097
4098 #[test]
4099 fn record_delimiter_is_for_a_sequential_file() {
4100 let program = |selects: &str| {
4101 let head = " IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n ENVIRONMENT DIVISION.\n INPUT-OUTPUT SECTION.\n FILE-CONTROL.\n";
4102 format!("{head}{selects} DATA DIVISION.\n FILE SECTION.\n FD S.\n 01 S-REC PIC X(80).\n PROCEDURE DIVISION.\n GOBACK.\n")
4103 };
4104 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}"));
4105 assert_eq!(p.files[0].organization, Organization::Sequential);
4106 crate::parse(&program(" SELECT S ASSIGN TO SDD RECORD DELIMITER TAPEDEL.\n")).unwrap_or_else(|e| panic!("{e}"));
4107 let err = crate::parse(&program(" SELECT S ASSIGN TO SDD ORGANIZATION LINE SEQUENTIAL\n RECORD DELIMITER STANDARD-1.\n")).unwrap_err();
4108 assert_eq!(err.message, "RECORD DELIMITER on S: the clause is for a file of ORGANIZATION SEQUENTIAL");
4109 }
4110}