Skip to main content

ironwork_syntax/
parser.rs

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