Skip to main content

ironwork_syntax/
parser.rs

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