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