Skip to main content

ironwork_syntax/
parser.rs

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