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