Skip to main content

ironwork_syntax/
parser.rs

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