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