Skip to main content

ironwork_syntax/
parser.rs

1use crate::ast::*;
2use crate::lexer::{Tok, Token};
3use crate::{Error, Pos};
4
5mod declaratives;
6mod oo;
7mod report;
8mod sort;
9
10/// Every program in the source, first to last, with nested programs after the one containing them.
11pub fn parse(tokens: &[Token], options: Vec<String>) -> Result<Vec<Program>, Error> {
12    let mut parser = Parser { tokens, at: 0, exec_declarations: Vec::new(), cics: false, sql: SqlState::default(), mnemonics: Vec::new(), debugging: false };
13    let mut programs = Vec::new();
14    parser.program(&options, &mut programs)?;
15    while parser.peek().is_some() {
16        if !parser.at_division(&["IDENTIFICATION", "ID"]) {
17            return Err(parser.error("another program, or the end of the source"));
18        }
19        parser.program(&options, &mut programs)?;
20    }
21    Ok(programs)
22}
23
24/// Words that begin a statement: a list of operands ends at any of them.
25const VERBS: &[&str] = &[
26    "MOVE", "COMPUTE", "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "IF", "PERFORM", "DISPLAY", "INITIALIZE", "GO", "GOBACK", "STOP",
27    "CONTINUE", "EXIT", "EVALUATE", "SET", "CALL", "ACCEPT", "STRING", "UNSTRING", "INSPECT", "READ", "WRITE", "OPEN", "CLOSE",
28    "REWRITE", "DELETE", "START", "SEARCH", "SORT", "MERGE", "RETURN", "RELEASE", "CANCEL", "EXEC", "NEXT", "INVOKE",
29    "INITIATE", "GENERATE", "TERMINATE", "SUPPRESS", "ALTER", "ENTRY",
30];
31
32/// Words that end a phrase or a nested block.
33const PHRASE_WORDS: &[&str] = &[
34    "ELSE", "END-IF", "END-PERFORM", "END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE", "END-DISPLAY", "WHEN",
35    "TO", "FROM", "BY", "INTO", "GIVING", "REMAINDER", "ROUNDED", "ON", "NOT", "SIZE", "UNTIL", "VARYING", "TIMES", "THRU", "THROUGH",
36    "AND", "OR", "THEN", "UPON", "WITH", "IS", "END-EVALUATE", "ALSO", "OTHER", "OF", "IN", "AT", "END", "END-READ", "END-WRITE",
37    "BEFORE", "AFTER", "ADVANCING", "INPUT", "OUTPUT", "EXTEND", "I-O", "REVERSED", "USING", "RETURNING", "EXCEPTION", "OVERFLOW",
38    "END-CALL", "OMITTED", "CONTENT", "REFERENCE", "VALUE", "UP", "DOWN", "DELIMITED", "DELIMITER", "COUNT", "POINTER", "TALLYING",
39    "REPLACING", "CONVERTING", "INITIAL", "FOR", "CHARACTERS", "LEADING", "FIRST", "ALL", "END-STRING", "END-UNSTRING", "END-SEARCH",
40    "NEXT", "INVALID", "KEY", "END-REWRITE", "END-DELETE", "END-START", "END-INVOKE", "END-RETURN", "END-OF-PAGE", "EOP",
41];
42
43/// The environment-names a WRITE ADVANCING mnemonic-name can stand for (Language Reference,
44/// SPECIAL-NAMES, Table 5): channels C01 to C12, CSP, pockets S01 to S05, and AFP-5A.
45fn advancing_environment_name(word: &str) -> bool {
46    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));
47    matches!(word, "CSP" | "AFP-5A") || numbered('C', 12) || numbered('S', 5)
48}
49
50fn figurative(word: &str) -> Option<Figurative> {
51    Some(match word {
52        "ZERO" | "ZEROS" | "ZEROES" => Figurative::Zero,
53        "SPACE" | "SPACES" => Figurative::Space,
54        "HIGH-VALUE" | "HIGH-VALUES" => Figurative::HighValue,
55        "LOW-VALUE" | "LOW-VALUES" => Figurative::LowValue,
56        "QUOTE" | "QUOTES" => Figurative::Quote,
57        "NULL" | "NULLS" => Figurative::Null,
58        _ => return None,
59    })
60}
61
62/// A contained program has the alphabets, collating sequence, decimal point and debugging mode of
63/// the program containing it, whose configuration section is the only one (Language Reference
64/// SC27-8713-03, p. 121).
65fn share_configuration(outer: &Environment, inner: &mut Environment) {
66    inner.debugging_mode |= outer.debugging_mode;
67    inner.decimal_point_comma |= outer.decimal_point_comma;
68    if inner.collating_sequence.is_none() {
69        inner.collating_sequence.clone_from(&outer.collating_sequence);
70    }
71    for (name, alphabet) in &outer.alphabets {
72        if !inner.alphabets.iter().any(|(n, _)| n == name) {
73            inner.alphabets.push((name.clone(), alphabet.clone()));
74        }
75    }
76}
77
78fn usage_word(word: &str) -> Option<Usage> {
79    Some(match word {
80        "DISPLAY" => Usage::Display,
81        "BINARY" | "COMP" | "COMPUTATIONAL" | "COMP-4" | "COMPUTATIONAL-4" => Usage::Binary,
82        "COMP-5" | "COMPUTATIONAL-5" => Usage::NativeBinary,
83        "PACKED-DECIMAL" | "COMP-3" | "COMPUTATIONAL-3" => Usage::Packed,
84        "COMP-1" | "COMPUTATIONAL-1" => Usage::Float1,
85        "COMP-2" | "COMPUTATIONAL-2" => Usage::Float2,
86        "NATIONAL" => Usage::National,
87        "POINTER" => Usage::Pointer,
88        "INDEX" => Usage::Index,
89        "FUNCTION-POINTER" | "PROCEDURE-POINTER" => Usage::ProgramPointer,
90        _ => return None,
91    })
92}
93
94struct Parser<'a> {
95    tokens: &'a [Token],
96    at: usize,
97    /// DATA DIVISION EXEC blocks of the program being parsed.
98    exec_declarations: Vec<ExecBlock>,
99    /// Whether the program being parsed has EXEC CICS, so the translator's additions apply.
100    cics: bool,
101    /// The WRITE ADVANCING mnemonic-names in scope: the program's own, then those of the programs
102    /// containing it, whose configuration section applies to it too.
103    mnemonics: Vec<(String, String)>,
104    sql: SqlState,
105    /// WITH DEBUGGING MODE, from the program's configuration section or its container's.
106    debugging: bool,
107}
108
109/// The WHENEVER actions in force, which carry on in listing order, and the EXEC SQL blocks the
110/// program being parsed has so far.
111#[derive(Default)]
112struct SqlState {
113    whenever: crate::sql::Whenever,
114    blocks: u32,
115    cursors: crate::sql::Cursors,
116}
117
118type R<T> = Result<T, Error>;
119
120impl Parser<'_> {
121    fn peek(&self) -> Option<&Tok> {
122        self.tokens.get(self.at).map(|t| &t.tok)
123    }
124
125    fn peek_at(&self, ahead: usize) -> Option<&Tok> {
126        self.tokens.get(self.at + ahead).map(|t| &t.tok)
127    }
128
129    fn pos(&self) -> Pos {
130        self.tokens.get(self.at).or(self.tokens.last()).map(|t| t.pos).unwrap_or_default()
131    }
132
133    fn word(&self) -> Option<&str> {
134        match self.peek() {
135            Some(Tok::Word(w)) => Some(w),
136            _ => None,
137        }
138    }
139
140    fn word_at(&self, ahead: usize) -> Option<&str> {
141        match self.peek_at(ahead) {
142            Some(Tok::Word(w)) => Some(w),
143            _ => None,
144        }
145    }
146
147    fn is_word(&self, w: &str) -> bool {
148        self.word() == Some(w)
149    }
150
151    fn accept_word(&mut self, w: &str) -> bool {
152        let yes = self.is_word(w);
153        if yes {
154            self.at += 1;
155        }
156        yes
157    }
158
159    fn accept_any(&mut self, words: &[&str]) -> Option<String> {
160        let w = self.word().filter(|w| words.contains(w))?.to_owned();
161        self.at += 1;
162        Some(w)
163    }
164
165    fn expect_word(&mut self, w: &str) -> R<()> {
166        if self.accept_word(w) { Ok(()) } else { Err(self.error(format!("expected {w}"))) }
167    }
168
169    fn accept(&mut self, tok: &Tok) -> bool {
170        let yes = self.peek() == Some(tok);
171        if yes {
172            self.at += 1;
173        }
174        yes
175    }
176
177    fn expect(&mut self, tok: &Tok, what: &str) -> R<()> {
178        if self.accept(tok) { Ok(()) } else { Err(self.error(format!("expected {what}"))) }
179    }
180
181    fn error(&self, message: impl Into<String>) -> Error {
182        let found = match self.peek() {
183            None => "end of source".to_owned(),
184            Some(Tok::Word(w)) => w.clone(),
185            Some(t) => format!("{t:?}"),
186        };
187        Error::at(self.pos(), format!("{}, found {found}", message.into()))
188    }
189
190    fn name(&mut self, what: &str) -> R<String> {
191        match self.peek() {
192            Some(Tok::Word(w)) => {
193                let w = w.clone();
194                self.at += 1;
195                Ok(w)
196            }
197            _ => Err(self.error(format!("expected {what}"))),
198        }
199    }
200
201    fn at_division(&self, names: &[&str]) -> bool {
202        self.word().is_some_and(|w| names.contains(&w)) && self.word_at(1) == Some("DIVISION")
203    }
204
205    fn program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
206        let outer = (std::mem::take(&mut self.exec_declarations), std::mem::take(&mut self.cics), std::mem::take(&mut self.sql.blocks), self.mnemonics.clone(), self.debugging);
207        let parsed = self.one_program(options, out);
208        (self.exec_declarations, self.cics, self.sql.blocks, self.mnemonics, self.debugging) = outer;
209        parsed
210    }
211
212    fn one_program(&mut self, options: &[String], out: &mut Vec<Program>) -> R<()> {
213        if !self.accept_word("IDENTIFICATION") {
214            self.expect_word("ID")?;
215        }
216        self.expect_word("DIVISION")?;
217        self.expect(&Tok::Period, "a period")?;
218        if self.is_word("CLASS-ID") {
219            return self.class_definition(options, out);
220        }
221        self.expect_word("PROGRAM-ID")?;
222        self.accept(&Tok::Period);
223        let id = match self.peek() {
224            Some(Tok::Alnum(s)) => {
225                let s = s.clone();
226                self.at += 1;
227                s
228            }
229            _ => self.name("a program name")?,
230        };
231        let (mut initial, mut recursive) = (false, false);
232        while let Some(t) = self.peek() {
233            if *t == Tok::Period {
234                self.at += 1;
235                break;
236            }
237            initial |= self.is_word("INITIAL");
238            recursive |= self.is_word("RECURSIVE");
239            self.at += 1;
240        }
241        self.program_body(id, initial, recursive, options, out, false)
242    }
243
244    /// The rest of a program, or of a method after its METHOD-ID paragraph; a method's END METHOD
245    /// is left for its class to read.
246    fn program_body(&mut self, id: String, initial: bool, recursive: bool, options: &[String], out: &mut Vec<Program>, method: bool) -> R<()> {
247        while self.peek().is_some() && !self.at_division(&["ENVIRONMENT", "DATA", "PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() {
248            self.at += 1;
249        }
250        let (mut files, mut repository, mut environment) = (Vec::new(), Vec::new(), Environment::default());
251        if self.at_division(&["ENVIRONMENT"]) {
252            (files, repository) = self.environment(&mut environment)?;
253        }
254        self.mnemonics.splice(0..0, environment.mnemonics.iter().cloned());
255        self.debugging |= environment.debugging_mode;
256        let (mut working_storage, mut local_storage, mut linkage) = (Vec::new(), Vec::new(), Vec::new());
257        let mut report_writer = crate::report::ReportWriter::default();
258        let mut declaratives = Declaratives::default();
259        if self.at_division(&["DATA"]) {
260            self.at += 2;
261            self.expect(&Tok::Period, "a period")?;
262            while !self.at_division(&["PROCEDURE", "IDENTIFICATION", "ID"]) && !self.at_end_program() && self.peek().is_some() {
263                if self.data_exec()? {
264                    continue;
265                }
266                let section = self.name("a DATA DIVISION section")?;
267                self.expect_word("SECTION")?;
268                self.expect(&Tok::Period, "a period")?;
269                match section.as_str() {
270                    "WORKING-STORAGE" => working_storage = self.data_entries()?,
271                    "LINKAGE" => linkage = self.data_entries()?,
272                    "LOCAL-STORAGE" => local_storage = self.data_entries()?,
273                    "FILE" => self.file_section(&mut files)?,
274                    "REPORT" => report_writer.reports.extend(self.report_section()?),
275                    other => return Err(self.error(format!("the {other} SECTION is not supported yet"))),
276                }
277            }
278        }
279        let (mut using, mut returning) = (Vec::new(), None);
280        let paragraphs = if self.at_division(&["PROCEDURE"]) {
281            self.at += 2;
282            if self.accept_word("USING") {
283                using = self.parameters()?;
284            }
285            if self.accept_word("RETURNING") {
286                returning = Some(self.name("a RETURNING item")?);
287            }
288            self.expect(&Tok::Period, "a period after the PROCEDURE DIVISION header")?;
289            self.procedure_paragraphs(&mut report_writer, &mut declaratives)?
290        } else {
291            Vec::new()
292        };
293        declaratives::debugging_sections_allowed(&declaratives, recursive, method)?;
294        if let Some(f) = files.iter().find(|f| f.assign.is_empty()) {
295            return Err(Error::at(f.pos, format!("{} has no SELECT ... ASSIGN", f.name)));
296        }
297        if self.cics {
298            self.translator_additions(&mut linkage, &mut using)?;
299        }
300        let exec_declarations = std::mem::take(&mut self.exec_declarations);
301        let (mut nested, mut contained) = (Vec::new(), Vec::new());
302        while self.at_division(&["IDENTIFICATION", "ID"]) {
303            let first = nested.len();
304            self.program(options, &mut nested)?;
305            contained.extend(nested.get(first).map(|p: &Program| p.id.clone()));
306        }
307        let entry = nested.iter().flat_map(|p: &Program| &p.paragraphs).flat_map(|p| &p.statements).find_map(|s| match s {
308            Stmt::Entry { pos, .. } => Some(*pos),
309            _ => None,
310        });
311        if let Some(pos) = entry {
312            return Err(Error::at(pos, "ENTRY cannot be used in a nested program"));
313        }
314        oo::share_repository(&repository, &mut nested)?;
315        for inner in &mut nested {
316            share_configuration(&environment, &mut inner.environment);
317        }
318        declaratives::contained_programs(&declaratives, &report_writer, &nested)?;
319        if !method && self.at_end_program() && self.word_at(1) == Some("PROGRAM") {
320            self.at += 2;
321            if self.word().is_some() || matches!(self.peek(), Some(Tok::Alnum(_))) {
322                self.at += 1;
323            }
324            self.accept(&Tok::Period);
325        }
326        out.push(Program {
327            id,
328            options: options.to_vec(),
329            initial,
330            recursive,
331            working_storage,
332            local_storage,
333            linkage,
334            using,
335            returning,
336            paragraphs,
337            files,
338            exec_declarations,
339            report_writer,
340            declaratives,
341            oo: oo::program_oo(repository),
342            environment,
343            nested: contained,
344            ..Program::default()
345        });
346        out.extend(nested);
347        Ok(())
348    }
349
350    /// The LINKAGE items of a PROCEDURE DIVISION or ENTRY USING list, each BY REFERENCE or BY VALUE.
351    fn parameters(&mut self) -> R<Vec<Param>> {
352        let (mut using, mut by_value) = (Vec::new(), false);
353        loop {
354            if self.accept_word("BY") {
355                by_value = self.accept_any(&["REFERENCE", "VALUE"]).as_deref() == Some("VALUE");
356                continue;
357            }
358            if !self.starts_ref() {
359                return Ok(using);
360            }
361            using.push(Param { by_value, name: self.name("a LINKAGE item")? });
362        }
363    }
364
365    fn at_end_program(&self) -> bool {
366        self.is_word("END") && matches!(self.word_at(1), Some("PROGRAM" | "METHOD"))
367    }
368
369    /// The ENVIRONMENT DIVISION: SELECT entries of FILE-CONTROL and the REPOSITORY's classes;
370    /// everything else is skipped, except what would change the meaning of the rest of the program.
371    fn environment(&mut self, clauses: &mut Environment) -> R<(Vec<FileDecl>, Vec<ClassEntry>)> {
372        let (mut files, mut repository) = (Vec::new(), Vec::new());
373        while self.peek().is_some() && !self.at_division(&["DATA", "PROCEDURE"]) {
374            if self.accept_word("DECIMAL-POINT") {
375                self.accept_word("IS");
376                self.expect_word("COMMA")?;
377                clauses.decimal_point_comma = true;
378                continue;
379            }
380            if self.is_word("CURRENCY") {
381                let pos = self.pos();
382                self.at += 1;
383                self.accept_word("SIGN");
384                self.accept_word("IS");
385                let sign = self.literal()?;
386                if sign != Literal::Alnum("$".into()) || self.is_word("WITH") || self.is_word("PICTURE") {
387                    return Err(Error::at(pos, "a CURRENCY SIGN other than $ is not supported yet"));
388                }
389                continue;
390            }
391            if self.environment_clause(clauses)? {
392                continue;
393            }
394            if self.is_word("DEBUGGING") && self.word_at(1) == Some("MODE") {
395                self.at += 2;
396                clauses.debugging_mode = true;
397                continue;
398            }
399            if let Some(environment) = self.word().filter(|w| advancing_environment_name(w)).map(str::to_owned) {
400                let at_name = if self.word_at(1) == Some("IS") { 2 } else { 1 };
401                if let Some(name) = self.word_at(at_name).map(str::to_owned) {
402                    self.at += at_name + 1;
403                    clauses.mnemonics.push((name, environment));
404                    continue;
405                }
406            }
407            if self.accept_word("SELECT") {
408                files.push(self.select()?);
409                continue;
410            }
411            if self.accept_word("REPOSITORY") {
412                repository = self.repository()?;
413                continue;
414            }
415            self.at += 1;
416        }
417        Ok((files, repository))
418    }
419
420    fn select(&mut self) -> R<FileDecl> {
421        let pos = self.pos();
422        let optional = self.accept_word("OPTIONAL");
423        let name = self.name("a file name")?;
424        let mut f = FileDecl {
425            name,
426            assign: String::new(),
427            organization: Organization::Sequential,
428            access: Access::Sequential,
429            record_key: None,
430            alternate_keys: Vec::new(),
431            relative_key: None,
432            optional,
433            status: None,
434            recording: None,
435            record_min: None,
436            record_max: None,
437            records: Vec::new(),
438            reports: Vec::new(),
439            linage: None,
440            sort: false,
441            pos,
442        };
443        while !self.accept(&Tok::Period) {
444            let clause = self.name("a SELECT clause or a period")?;
445            match clause.as_str() {
446                "ASSIGN" => {
447                    self.accept_word("TO");
448                    let target = match self.peek().cloned() {
449                        Some(Tok::Word(w)) | Some(Tok::Alnum(w)) => w,
450                        _ => return Err(self.error("a DD name after ASSIGN")),
451                    };
452                    self.at += 1;
453                    let target = target.to_ascii_uppercase();
454                    f.assign = target.rsplit('-').next().filter(|_| target.contains("-S-") || target.starts_with("S-") || target.starts_with("AS-")).unwrap_or(&target).to_owned();
455                }
456                "RECORD" if !self.is_word("SEQUENTIAL") => {
457                    self.accept_word("KEY");
458                    self.accept_word("IS");
459                    f.record_key = Some(self.reference()?);
460                }
461                "RELATIVE" if self.is_word("KEY") => {
462                    self.at += 1;
463                    self.accept_word("IS");
464                    f.relative_key = Some(self.reference()?);
465                }
466                "ALTERNATE" => {
467                    self.accept_word("RECORD");
468                    self.expect_word("KEY")?;
469                    self.accept_word("IS");
470                    let key = self.reference()?;
471                    let duplicates = self.accept_word("WITH") | self.is_word("DUPLICATES");
472                    if duplicates {
473                        self.expect_word("DUPLICATES")?;
474                    }
475                    f.alternate_keys.push((key, duplicates));
476                }
477                "ORGANIZATION" | "LINE" | "RECORD" | "SEQUENTIAL" | "INDEXED" | "RELATIVE" => {
478                    if clause == "ORGANIZATION" {
479                        self.accept_word("IS");
480                    }
481                    let first = if clause == "ORGANIZATION" { self.name("an organization")? } else { clause.clone() };
482                    f.organization = match first.as_str() {
483                        "LINE" => {
484                            self.expect_word("SEQUENTIAL")?;
485                            Organization::LineSequential
486                        }
487                        "RECORD" => {
488                            self.expect_word("SEQUENTIAL")?;
489                            Organization::Sequential
490                        }
491                        "SEQUENTIAL" => Organization::Sequential,
492                        "INDEXED" => Organization::Indexed,
493                        "RELATIVE" => Organization::Relative,
494                        other => return Err(self.error(format!("ORGANIZATION {other} is not supported yet"))),
495                    };
496                }
497                "ACCESS" => {
498                    self.accept_word("MODE");
499                    self.accept_word("IS");
500                    f.access = match self.name("an access mode")?.as_str() {
501                        "SEQUENTIAL" => Access::Sequential,
502                        "RANDOM" => Access::Random,
503                        "DYNAMIC" => Access::Dynamic,
504                        other => return Err(self.error(format!("ACCESS MODE {other} is not an access mode"))),
505                    };
506                }
507                "FILE" | "STATUS" => {
508                    if clause == "FILE" {
509                        self.expect_word("STATUS")?;
510                    }
511                    self.accept_word("IS");
512                    f.status = Some(self.reference()?);
513                    if self.starts_ref() && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
514                        self.reference()?;
515                    }
516                }
517                "RESERVE" | "PADDING" => {
518                    while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| SELECT_CLAUSES.contains(&w)) {
519                        self.at += 1;
520                    }
521                }
522                other => return Err(self.error(format!("{other} is not a SELECT clause ironwork for COBOL supports yet"))),
523            }
524        }
525        Ok(f)
526    }
527
528    /// FD entries: the clauses that shape records are kept, the rest skipped; then each file's
529    /// record descriptions.
530    fn file_section(&mut self, files: &mut [FileDecl]) -> R<()> {
531        while self.is_word("FD") || self.is_word("SD") {
532            let indicator = self.name("FD or SD")?;
533            let pos = self.pos();
534            let name = self.name("a file name")?;
535            let Some(index) = files.iter().position(|f| f.name == name) else {
536                return Err(Error::at(pos, format!("{indicator} {name} has no SELECT")));
537            };
538            files[index].sort = indicator == "SD";
539            while !self.accept(&Tok::Period) {
540                match self.name("an FD clause or a period")?.as_str() {
541                    "RECORDING" => {
542                        self.accept_word("MODE");
543                        self.accept_word("IS");
544                        let mode = self.name("F, V, U or S")?;
545                        files[index].recording = mode.chars().next();
546                    }
547                    "RECORD" => {
548                        self.accept_word("CONTAINS");
549                        self.accept_word("IS");
550                        if self.accept_word("VARYING") {
551                            files[index].recording.get_or_insert('V');
552                            self.accept_word("IN");
553                            self.accept_word("SIZE");
554                            self.accept_word("FROM");
555                        }
556                        let number = |p: &mut Self| -> R<Option<u32>> {
557                            match p.peek() {
558                                Some(Tok::Number(n)) => {
559                                    let v = n.parse().map_err(|_| p.error("a record length"))?;
560                                    p.at += 1;
561                                    Ok(Some(v))
562                                }
563                                _ => Ok(None),
564                            }
565                        };
566                        let first = number(self)?;
567                        let second = if self.accept_word("TO") { number(self)? } else { None };
568                        if first.is_some() {
569                            files[index].record_min = first;
570                            files[index].record_max = second.or(first);
571                        }
572                        self.accept_word("CHARACTERS");
573                        if self.accept_word("DEPENDING") {
574                            self.accept_word("ON");
575                            self.reference()?;
576                        }
577                    }
578                    "REPORT" | "REPORTS" if files[index].sort => return Err(Error::at(pos, format!("SD {name}: a sort or merge file takes no REPORT clause"))),
579                    "REPORT" | "REPORTS" => {
580                        let names = self.report_names()?;
581                        files[index].reports.extend(names);
582                    }
583                    "LINAGE" => {
584                        let linage = self.linage()?;
585                        if files[index].linage.is_some() {
586                            return Err(Error::at(pos, format!("{indicator} {name}: LINAGE is given twice")));
587                        }
588                        files[index].linage = (indicator == "FD").then_some(linage);
589                    }
590                    _ => {
591                        while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.word().is_some_and(|w| FD_WORDS.contains(&w)) {
592                            self.at += 1;
593                        }
594                    }
595                }
596            }
597            files[index].records = self.data_entries()?;
598        }
599        Ok(())
600    }
601
602    /// The LINAGE clause after its keyword; FOOTING, TOP and BOTTOM are taken in any order.
603    fn linage(&mut self) -> R<Linage> {
604        self.accept_word("IS");
605        let lines = self.linage_value("LINAGE")?;
606        self.accept_word("LINES");
607        let mut linage = Linage { lines, footing: None, top: None, bottom: None };
608        loop {
609            let start = self.at;
610            self.accept_word("WITH");
611            self.accept_word("LINES");
612            self.accept_word("AT");
613            let Some(phrase) = self.accept_any(&["FOOTING", "TOP", "BOTTOM"]) else {
614                self.at = start;
615                return Ok(linage);
616            };
617            if phrase == "FOOTING" {
618                self.accept_word("AT");
619            }
620            let value = Some(self.linage_value(&phrase)?);
621            let slot = match phrase.as_str() {
622                "FOOTING" => &mut linage.footing,
623                "TOP" => &mut linage.top,
624                _ => &mut linage.bottom,
625            };
626            if slot.is_some() {
627                return Err(self.error(format!("LINAGE: {phrase} is given twice")));
628            }
629            *slot = value;
630        }
631    }
632
633    /// An unsigned integer, or a data-name that may be qualified.
634    fn linage_value(&mut self, phrase: &str) -> R<LinageValue> {
635        match self.peek() {
636            Some(Tok::Number(n)) if n.bytes().all(|b| b.is_ascii_digit()) => {
637                let n = n.clone();
638                self.at += 1;
639                Ok(LinageValue::Integer(n))
640            }
641            Some(Tok::Number(n)) => Err(self.error(format!("LINAGE: {phrase} {n} is not an unsigned integer"))),
642            Some(Tok::Word(_)) if self.starts_ref() => {
643                let r = self.reference()?;
644                if !r.subscripts.is_empty() || r.refmod.is_some() {
645                    return Err(Error::at(r.pos, format!("LINAGE: {phrase} {} takes no subscript or reference modification", r.name)));
646                }
647                Ok(LinageValue::Data(r))
648            }
649            _ => Err(self.error(format!("an integer or a data-name after {phrase}"))),
650        }
651    }
652
653    /// An EXEC block among DATA DIVISION entries, kept as a declaration with its period.
654    fn data_exec(&mut self) -> R<bool> {
655        let Some(Tok::Exec(text)) = self.peek().cloned() else { return Ok(false) };
656        let block = self.exec_block(&text, self.pos());
657        self.at += 1;
658        self.accept(&Tok::Period);
659        self.exec_declarations.push(block);
660        Ok(true)
661    }
662
663    /// What the CICS translator adds: the EXEC interface block, a one-byte DFHCOMMAREA when the
664    /// program declares none, and both at the head of PROCEDURE DIVISION USING.
665    fn translator_additions(&mut self, linkage: &mut Vec<DataEntry>, using: &mut Vec<Param>) -> R<()> {
666        let defined = |l: &[DataEntry], n: &str| l.iter().any(|e| e.level == 1 && e.name.as_deref() == Some(n));
667        let mut added = Vec::new();
668        if !defined(linkage, "DFHEIBLK") {
669            added.extend(system_entries("DFHEIBLK")?);
670        }
671        if !defined(linkage, "DFHCOMMAREA") {
672            added.extend(system_text_entries("       01  DFHCOMMAREA PIC X(1).\n")?);
673        }
674        linkage.splice(0..0, added);
675        for (i, name) in ["DFHEIBLK", "DFHCOMMAREA"].into_iter().enumerate() {
676            if using.get(i).map(|p| p.name.as_str()) != Some(name) {
677                using.insert(i, Param { by_value: false, name: name.into() });
678            }
679        }
680        Ok(())
681    }
682
683    fn exec_block(&mut self, text: &str, pos: Pos) -> ExecBlock {
684        let (kind_word, body) = text.split_once(' ').unwrap_or((text, ""));
685        let kind = match kind_word.to_ascii_uppercase().as_str() {
686            "SQL" => ExecKind::Sql,
687            "CICS" => ExecKind::Cics,
688            "DLI" => ExecKind::Dli,
689            _ => ExecKind::Other,
690        };
691        let words: Vec<String> = body.split_whitespace().map(|w| w.to_ascii_uppercase()).collect();
692        let word = |i: usize| words.get(i).map(String::as_str).unwrap_or("");
693        let mut block = ExecBlock { kind, command: word(0).to_owned(), options: Vec::new(), host_variables: Vec::new(), sql: None, text: text.to_owned(), pos };
694        match kind {
695            ExecKind::Sql => {
696                block.command = match (word(0), word(1), word(2)) {
697                    ("DECLARE", _, "CURSOR") => "DECLARE CURSOR".into(),
698                    ("DECLARE", _, "TABLE") => "DECLARE TABLE".into(),
699                    ("DECLARE", _, "STATEMENT") => "DECLARE STATEMENT".into(),
700                    ("BEGIN" | "END", "DECLARE", "SECTION") => format!("{} DECLARE SECTION", word(0)),
701                    (first, _, _) => first.into(),
702                };
703                let statement = self.sql.cursors.resolve(crate::sql::parse(body, pos));
704                if let crate::sql::Statement::Whenever { condition, action } = &statement {
705                    self.sql.whenever.set(*condition, action.clone());
706                }
707                block.host_variables = match &statement {
708                    crate::sql::Statement::Unsupported(_)
709                    | crate::sql::Statement::Malformed(_)
710                    | crate::sql::Statement::Declaration
711                    | crate::sql::Statement::DeclareUnsupported { .. } => host_variables(body, pos),
712                    typed => typed.references().into_iter().cloned().collect(),
713                };
714                self.sql.blocks += 1;
715                block.sql = Some(crate::sql::Sql { statement, ordinal: self.sql.blocks, whenever: self.sql.whenever.clone() });
716            }
717            ExecKind::Cics => {
718                self.cics = true;
719                block.options = cics_options(body);
720                if let Some((first, None)) = block.options.first().cloned() {
721                    block.command = first.clone();
722                    block.options.remove(0);
723                    if let Some((second, None)) = block.options.first().cloned()
724                        && crate::system::cics_two_word(&first, &second)
725                    {
726                        block.command = format!("{first} {second}");
727                        block.options.remove(0);
728                    }
729                }
730                let labels = block.command.starts_with("HANDLE");
731                for (_, arg) in &mut block.options {
732                    if let Some(ExecArg::Text(t)) = arg
733                        && !labels
734                        && let Some(op) = operand_of(t, pos)
735                    {
736                        *arg = Some(ExecArg::Operand(op));
737                    }
738                }
739            }
740            _ => {}
741        }
742        block
743    }
744
745    fn data_entries(&mut self) -> R<Vec<DataEntry>> {
746        let mut entries = Vec::new();
747        loop {
748            if self.data_exec()? {
749                continue;
750            }
751            let Some((level, pos)) = self.level_number()? else { break };
752            entries.push(self.data_entry(level, pos)?);
753        }
754        Ok(entries)
755    }
756
757    fn level_number(&mut self) -> R<Option<(u8, Pos)>> {
758        let Some(Tok::Number(level)) = self.peek() else { return Ok(None) };
759        let pos = self.pos();
760        let level = level.parse().map_err(|_| self.error("a level number"))?;
761        self.at += 1;
762        Ok(Some((level, pos)))
763    }
764
765    /// The character-string after PICTURE.
766    fn picture(&mut self) -> R<String> {
767        self.accept_word("IS");
768        match self.peek() {
769            Some(Tok::Pic(p)) => {
770                let p = p.clone();
771                self.at += 1;
772                Ok(p)
773            }
774            _ => Err(self.error("a PICTURE character-string")),
775        }
776    }
777
778    /// The SIGN clause, after its first word: SIGN, LEADING or TRAILING.
779    fn sign_clause(&mut self, first: &str) -> R<SignClause> {
780        let side = if first == "SIGN" {
781            self.accept_word("IS");
782            self.name("LEADING or TRAILING")?
783        } else {
784            first.to_owned()
785        };
786        let position = match side.as_str() {
787            "LEADING" => SignPosition::Leading,
788            "TRAILING" => SignPosition::Trailing,
789            _ => return Err(self.error("LEADING or TRAILING")),
790        };
791        let separate = self.accept_word("SEPARATE");
792        if separate {
793            self.accept_word("CHARACTER");
794        }
795        Ok(SignClause { position, separate })
796    }
797
798    /// WHEN ZERO, after BLANK.
799    fn blank_when_zero(&mut self) -> R<()> {
800        self.accept_word("WHEN");
801        if self.accept_any(&["ZERO", "ZEROS", "ZEROES"]).is_none() {
802            return Err(self.error("ZERO after BLANK WHEN"));
803        }
804        Ok(())
805    }
806
807    fn data_entry(&mut self, level: u8, pos: Pos) -> R<DataEntry> {
808        let mut e = DataEntry {
809            level,
810            name: None,
811            picture: None,
812            usage: None,
813            value: None,
814            redefines: None,
815            occurs: None,
816            depending_on: None,
817            sign: None,
818            justified: false,
819            sync: false,
820            blank_when_zero: false,
821            indexed_by: Vec::new(),
822            keys: Vec::new(),
823            condition_values: Vec::new(),
824            false_value: None,
825            renames: None,
826            object_class: None,
827            pos,
828        };
829        if let Some(w) = self.word()
830            && !is_clause_word(w)
831        {
832            if w != "FILLER" {
833                e.name = Some(w.to_owned());
834            }
835            self.at += 1;
836        }
837        while !self.accept(&Tok::Period) {
838            let clause = self.name("a data description clause or a period")?;
839            match clause.as_str() {
840                "PIC" | "PICTURE" => e.picture = Some(self.picture()?),
841                "USAGE" => {
842                    self.accept_word("IS");
843                    let w = self.name("a usage")?;
844                    if w == "OBJECT" {
845                        self.object_reference(&mut e)?;
846                    } else {
847                        e.usage = Some(usage_word(&w).ok_or_else(|| Error::at(pos, format!("USAGE {w} is not supported yet")))?);
848                    }
849                }
850                "OBJECT" => self.object_reference(&mut e)?,
851                "VALUE" | "VALUES" => {
852                    self.accept_word("IS");
853                    self.accept_word("ARE");
854                    if level == 88 {
855                        while self.peek().is_some() && self.peek() != Some(&Tok::Period) && !self.is_word("WHEN") && !self.is_word("FALSE") {
856                            let low = self.literal()?;
857                            let high = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.literal()?) } else { None };
858                            e.condition_values.push((low, high));
859                        }
860                        if self.accept_word("WHEN") {
861                            self.accept_word("SET");
862                            self.accept_word("TO");
863                            self.expect_word("FALSE")?;
864                            self.accept_word("IS");
865                            e.false_value = Some(self.literal()?);
866                        } else if self.accept_word("FALSE") {
867                            self.accept_word("IS");
868                            e.false_value = Some(self.literal()?);
869                        }
870                    } else {
871                        e.value = Some(self.literal()?);
872                    }
873                }
874                "REDEFINES" => e.redefines = Some(self.name("the item redefined")?),
875                "RENAMES" => {
876                    let first = self.reference()?;
877                    let last = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.reference()?) } else { None };
878                    e.renames = Some((first, last));
879                }
880                "OCCURS" => {
881                    let count = |p: &mut Self| -> R<u32> {
882                        let n = match p.peek() {
883                            Some(Tok::Number(n)) => n.parse().map_err(|_| p.error("an OCCURS count"))?,
884                            _ => return Err(p.error("an OCCURS count")),
885                        };
886                        p.at += 1;
887                        Ok(n)
888                    };
889                    let mut most = count(self)?;
890                    if self.accept_word("TO") {
891                        most = count(self)?;
892                    }
893                    e.occurs = Some(most);
894                    self.accept_word("TIMES");
895                    if self.accept_word("DEPENDING") {
896                        self.accept_word("ON");
897                        e.depending_on = Some(self.reference()?);
898                    }
899                    loop {
900                        if let Some(order) = self.accept_any(&["ASCENDING", "DESCENDING"]) {
901                            self.accept_word("KEY");
902                            self.accept_word("IS");
903                            while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "INDEXED" | "ASCENDING" | "DESCENDING")) {
904                                e.keys.push((order == "ASCENDING", self.reference()?));
905                            }
906                        } else if self.accept_word("INDEXED") {
907                            self.accept_word("BY");
908                            while self.word().is_some_and(|w| !is_clause_word(w) && !matches!(w, "ASCENDING" | "DESCENDING")) {
909                                e.indexed_by.push(self.name("an index name")?);
910                            }
911                        } else {
912                            break;
913                        }
914                    }
915                }
916                "SIGN" | "LEADING" | "TRAILING" => e.sign = Some(self.sign_clause(&clause)?),
917                "JUSTIFIED" | "JUST" => {
918                    self.accept_word("RIGHT");
919                    e.justified = true;
920                }
921                "SYNC" | "SYNCHRONIZED" => {
922                    self.accept_any(&["LEFT", "RIGHT"]);
923                    e.sync = true;
924                }
925                "BLANK" => {
926                    self.blank_when_zero()?;
927                    e.blank_when_zero = true;
928                }
929                "GLOBAL" | "EXTERNAL" => {}
930                other => match usage_word(other) {
931                    Some(u) => e.usage = Some(u),
932                    None => return Err(Error::at(self.tokens[self.at - 1].pos, format!("{other} is not a data description clause ironwork for COBOL supports yet"))),
933                },
934            }
935        }
936        Ok(e)
937    }
938
939    fn literal(&mut self) -> R<Literal> {
940        let lit = match self.peek().cloned() {
941            Some(Tok::Alnum(s)) => Literal::Alnum(s),
942            Some(Tok::Hex(b)) => Literal::Hex(b),
943            Some(Tok::National(s)) => Literal::National(s),
944            Some(Tok::Number(n)) => Literal::Number(n),
945            Some(Tok::Word(w)) if w == "ALL" => {
946                self.at += 1;
947                return Ok(Literal::All(Box::new(self.literal()?)));
948            }
949            Some(Tok::Word(w)) => Literal::Figurative(figurative(&w).ok_or_else(|| self.error("a literal"))?),
950            _ => return Err(self.error("a literal")),
951        };
952        self.at += 1;
953        Ok(lit)
954    }
955
956    fn paragraph_header(&self) -> bool {
957        self.tokens.get(self.at).is_some_and(|t| t.area_a && matches!(t.tok, Tok::Word(_))) && self.peek_at(1) == Some(&Tok::Period)
958    }
959
960    fn section_header(&self) -> bool {
961        // EXIT SECTION is a statement; EXIT, reserved, names no section.
962        self.tokens.get(self.at).is_some_and(|t| matches!(t.tok, Tok::Word(_))) && self.word_at(0) != Some("EXIT") && self.word_at(1) == Some("SECTION")
963    }
964
965    fn paragraphs(&mut self) -> R<Vec<Paragraph>> {
966        let mut paragraphs = Vec::new();
967        while self.peek().is_some() && !self.at_end_program() && !self.at_division(&["IDENTIFICATION", "ID"]) {
968            if self.is_word("DECLARATIVES") {
969                return Err(self.error("DECLARATIVES must begin the PROCEDURE DIVISION"));
970            }
971            self.procedure_item(&mut paragraphs)?;
972        }
973        Ok(paragraphs)
974    }
975
976    /// A section or paragraph header, a separator period, or statements, added to `paragraphs`; true for a section header.
977    fn procedure_item(&mut self, paragraphs: &mut Vec<Paragraph>) -> R<bool> {
978        if self.section_header() {
979            let pos = self.pos();
980            let name = self.name("a section name")?;
981            self.at += 1;
982            let mut priority = 0;
983            if let Some(Tok::Number(n)) = self.peek() {
984                priority = n.trim_start_matches('+').parse().ok().filter(|p| *p <= 99).ok_or_else(|| self.error("a priority-number from 0 to 99"))?;
985                self.at += 1;
986            }
987            self.expect(&Tok::Period, "a period after the section header")?;
988            paragraphs.push(Paragraph { section: Some(name.clone()), name, statements: Vec::new(), is_section: true, priority, pos });
989            return Ok(true);
990        }
991        if self.paragraph_header() {
992            let pos = self.pos();
993            let name = self.name("a paragraph name")?;
994            self.at += 1;
995            let (section, priority) = paragraphs.last().map_or((None, 0), |p| (p.section.clone(), p.priority));
996            paragraphs.push(Paragraph { name, statements: Vec::new(), section, is_section: false, priority, pos });
997            return Ok(false);
998        }
999        if self.accept(&Tok::Period) {
1000            if let Some(p) = paragraphs.last_mut()
1001                && p.statements.last().is_some_and(|s| *s != Stmt::SentenceEnd)
1002            {
1003                p.statements.push(Stmt::SentenceEnd);
1004            }
1005            return Ok(false);
1006        }
1007        let block = self.block(&[])?;
1008        if block.is_empty() {
1009            return Err(self.error("a statement"));
1010        }
1011        if paragraphs.is_empty() {
1012            paragraphs.push(Paragraph { name: String::new(), statements: Vec::new(), section: None, is_section: false, priority: 0, pos: self.pos() });
1013        }
1014        paragraphs.last_mut().unwrap().statements.extend(block);
1015        Ok(false)
1016    }
1017
1018    /// Statements up to a period, a paragraph header, or one of `stops`, none of them consumed.
1019    fn block(&mut self, stops: &[&str]) -> R<Vec<Stmt>> {
1020        let mut out = Vec::new();
1021        while let Some(tok) = self.peek() {
1022            if *tok == Tok::Period || self.paragraph_header() || self.section_header() || self.word().is_some_and(|w| stops.contains(&w)) {
1023                break;
1024            }
1025            if let Some(Tok::Exec(text)) = self.peek().cloned() {
1026                let block = self.exec_block(&text, self.pos());
1027                self.at += 1;
1028                out.push(Stmt::Exec(Box::new(block)));
1029                continue;
1030            }
1031            if !self.word().is_some_and(|w| VERBS.contains(&w)) {
1032                break;
1033            }
1034            out.push(self.statement()?);
1035        }
1036        Ok(out)
1037    }
1038
1039    fn statement(&mut self) -> R<Stmt> {
1040        let pos = self.pos();
1041        let verb = self.name("a statement")?;
1042        Ok(match verb.as_str() {
1043            "MOVE" => {
1044                if self.accept_any(&["CORRESPONDING", "CORR"]).is_some() {
1045                    return Err(Error::at(pos, "MOVE CORRESPONDING is not supported yet"));
1046                }
1047                let from = self.operand()?;
1048                self.expect_word("TO")?;
1049                let to = self.refs()?;
1050                Stmt::Move { from, to, pos }
1051            }
1052            "COMPUTE" => {
1053                let targets = self.targets()?;
1054                if !self.accept(&Tok::Eq) {
1055                    self.expect_word("EQUAL")?;
1056                }
1057                let expr = self.expr()?;
1058                let size_error = self.size_error()?;
1059                self.accept_word("END-COMPUTE");
1060                Stmt::Compute { targets, expr, size_error, pos }
1061            }
1062            "ADD" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" => Stmt::Arith(Box::new(self.arith(&verb, pos)?)),
1063            "IF" => {
1064                let cond = self.cond()?;
1065                self.accept_word("THEN");
1066                let then = self.block(&["ELSE", "END-IF"])?;
1067                let otherwise = if self.accept_word("ELSE") { self.block(&["END-IF"])? } else { Vec::new() };
1068                self.accept_word("END-IF");
1069                Stmt::If { cond, then, otherwise, pos }
1070            }
1071            "PERFORM" => self.perform(pos)?,
1072            "DISPLAY" => {
1073                let mut items = Vec::new();
1074                let no_advancing_ahead = |p: &Self| p.is_word("NO") && p.word_at(1) == Some("ADVANCING");
1075                while self.starts_operand() && !no_advancing_ahead(self) {
1076                    items.push(self.operand()?);
1077                }
1078                if self.accept_word("UPON") {
1079                    self.name("a mnemonic name")?;
1080                }
1081                let no_advancing = self.accept_word("WITH") | no_advancing_ahead(self);
1082                if no_advancing {
1083                    self.expect_word("NO")?;
1084                    self.expect_word("ADVANCING")?;
1085                    if self.is_word("UPON") {
1086                        return Err(self.error("the end of DISPLAY: Enterprise COBOL takes UPON before WITH NO ADVANCING"));
1087                    }
1088                }
1089                self.accept_word("END-DISPLAY");
1090                Stmt::Display { items, no_advancing, pos }
1091            }
1092            "INITIALIZE" => Stmt::Initialize { targets: self.refs()?, pos },
1093            "CALL" => Stmt::Call(Box::new(self.call(pos)?)),
1094            "INVOKE" => Stmt::Invoke(Box::new(self.invoke(pos)?)),
1095            "CANCEL" => {
1096                let mut targets = Vec::new();
1097                while self.starts_operand() {
1098                    targets.push(self.operand()?);
1099                }
1100                if targets.is_empty() {
1101                    return Err(self.error("a program to CANCEL"));
1102                }
1103                Stmt::Cancel { targets, pos }
1104            }
1105            "SET" => Stmt::Set { set: self.set()?, pos },
1106            "STRING" => Stmt::String(Box::new(self.string(pos)?)),
1107            "UNSTRING" => Stmt::Unstring(Box::new(self.unstring(pos)?)),
1108            "INSPECT" => Stmt::Inspect(Box::new(self.inspect(pos)?)),
1109            "SEARCH" => Stmt::Search(Box::new(self.search(pos)?)),
1110            "SORT" | "MERGE" | "RELEASE" | "RETURN" => Stmt::Sorting(Box::new(self.sorting(&verb, pos)?)),
1111            "NEXT" => {
1112                self.expect_word("SENTENCE")?;
1113                Stmt::NextSentence
1114            }
1115            "ACCEPT" => {
1116                let target = self.reference()?;
1117                if self.is_word("FROM") && self.word_at(1) == Some("ENVIRONMENT") {
1118                    return Err(Error::at(pos, "ACCEPT ... FROM ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's"));
1119                }
1120                let from = if self.accept_word("FROM") {
1121                    match self.name("SYSIN, DATE, DAY, DAY-OF-WEEK or TIME")?.as_str() {
1122                        "DATE" => AcceptFrom::Date { four_digit_year: self.accept_word("YYYYMMDD") },
1123                        "DAY" => AcceptFrom::Day { four_digit_year: self.accept_word("YYYYDDD") },
1124                        "DAY-OF-WEEK" => AcceptFrom::DayOfWeek,
1125                        "TIME" => AcceptFrom::Time,
1126                        _ => AcceptFrom::Sysin,
1127                    }
1128                } else {
1129                    AcceptFrom::Sysin
1130                };
1131                self.accept_word("END-ACCEPT");
1132                Stmt::Accept { target, from, pos }
1133            }
1134            "OPEN" => {
1135                let mut files = Vec::new();
1136                while let Some(mode) = self.accept_any(&["INPUT", "OUTPUT", "EXTEND", "I-O"]) {
1137                    let mode = match mode.as_str() {
1138                        "INPUT" => OpenMode::Input,
1139                        "OUTPUT" => OpenMode::Output,
1140                        "EXTEND" => OpenMode::Extend,
1141                        _ => OpenMode::InputOutput,
1142                    };
1143                    while self.starts_ref() {
1144                        files.push((mode, self.name("a file name")?));
1145                        self.accept_any(&["REVERSED"]);
1146                        if self.accept_word("WITH") {
1147                            self.expect_word("NO")?;
1148                            self.expect_word("REWIND")?;
1149                        }
1150                    }
1151                }
1152                if files.is_empty() {
1153                    return Err(self.error("INPUT, OUTPUT, EXTEND or I-O and a file"));
1154                }
1155                Stmt::Open { files, pos }
1156            }
1157            "CLOSE" => {
1158                let mut files = Vec::new();
1159                while self.starts_ref() {
1160                    files.push(self.name("a file name")?);
1161                    if self.accept_word("WITH") {
1162                        self.accept_any(&["LOCK", "NO"]);
1163                        self.accept_word("REWIND");
1164                    }
1165                }
1166                Stmt::Close { files, pos }
1167            }
1168            "READ" => {
1169                let file = self.name("a file name")?;
1170                let previous = self.accept_word("PREVIOUS");
1171                let next = previous || self.accept_word("NEXT");
1172                self.accept_word("RECORD");
1173                let into = if self.accept_word("INTO") { Some(self.reference()?) } else { None };
1174                let key = if self.accept_word("KEY") {
1175                    self.accept_word("IS");
1176                    Some(self.reference()?)
1177                } else {
1178                    None
1179                };
1180                let [at_end, invalid] = self.on_phrases(&["AT", "END", "INVALID"], &["END-READ"], |p| {
1181                    if p.accept_word("INVALID") {
1182                        p.accept_word("KEY");
1183                        return Ok(1);
1184                    }
1185                    p.accept_word("AT");
1186                    p.expect_word("END").map(|()| 0)
1187                })?;
1188                self.accept_word("END-READ");
1189                Stmt::Read(Box::new(ReadStmt { file, next, previous, into, key, at_end, invalid, pos }))
1190            }
1191            "REWRITE" => {
1192                let record = self.reference()?;
1193                let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
1194                let invalid = self.invalid_key("END-REWRITE")?;
1195                Stmt::Rewrite { record, from, invalid, pos }
1196            }
1197            "DELETE" => {
1198                let file = self.name("a file name")?;
1199                self.accept_word("RECORD");
1200                let invalid = self.invalid_key("END-DELETE")?;
1201                Stmt::Delete { file, invalid, pos }
1202            }
1203            "START" => {
1204                let file = self.name("a file name")?;
1205                let key = if self.accept_word("KEY") {
1206                    self.accept_word("IS");
1207                    let op = if self.accept_word("NOT") {
1208                        match self.relop()? {
1209                            Some(RelOp::Lt) => RelOp::Ge,
1210                            _ => return Err(self.error("NOT < in START KEY")),
1211                        }
1212                    } else {
1213                        self.relop()?.ok_or_else(|| self.error("a relation after START KEY"))?
1214                    };
1215                    Some((op, self.reference()?))
1216                } else {
1217                    None
1218                };
1219                let invalid = self.invalid_key("END-START")?;
1220                Stmt::Start { file, key, invalid, pos }
1221            }
1222            "WRITE" => {
1223                let record = self.reference()?;
1224                let from = if self.accept_word("FROM") { Some(self.operand()?) } else { None };
1225                let mut advancing = None;
1226                if let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
1227                    let before = side == "BEFORE";
1228                    self.accept_word("ADVANCING");
1229                    let mnemonic = self.word().and_then(|w| self.mnemonics.iter().find(|(name, _)| name == w)).cloned();
1230                    advancing = Some(if self.accept_word("PAGE") {
1231                        Advancing::Page { before }
1232                    } else if let Some((name, environment)) = mnemonic {
1233                        self.at += 1;
1234                        Advancing::Mnemonic { before, name, environment }
1235                    } else {
1236                        let count = self.expr()?;
1237                        self.accept_any(&["LINE", "LINES"]);
1238                        Advancing::Lines { before, count }
1239                    });
1240                }
1241                let eop_ahead = |p: &Self, i: usize| {
1242                    let at = usize::from(p.word_at(i) == Some("AT"));
1243                    matches!(p.word_at(i + at), Some("END-OF-PAGE" | "EOP"))
1244                };
1245                let [end_of_page, invalid] = self.phrases_opening(|p, i| eop_ahead(p, i) || p.word_at(i) == Some("INVALID"), &["END-WRITE"], |p| {
1246                    if p.accept_word("INVALID") {
1247                        p.accept_word("KEY");
1248                        return Ok(1);
1249                    }
1250                    p.accept_word("AT");
1251                    p.at += 1;
1252                    Ok(0)
1253                })?;
1254                self.accept_word("END-WRITE");
1255                Stmt::Write { record, from, advancing, invalid, end_of_page, pos }
1256            }
1257            "GO" => {
1258                self.accept_word("TO");
1259                if self.peek() == Some(&Tok::Period) {
1260                    return Ok(Stmt::GoTo { target: None, pos });
1261                }
1262                let target = self.proc_name()?;
1263                if !self.starts_ref() && !self.is_word("DEPENDING") {
1264                    return Ok(Stmt::GoTo { target: Some(target), pos });
1265                }
1266                let mut targets = vec![target];
1267                while self.starts_ref() && !self.is_word("DEPENDING") {
1268                    targets.push(self.proc_name()?);
1269                }
1270                self.expect_word("DEPENDING")?;
1271                self.accept_word("ON");
1272                Stmt::GoToDepending { targets, on: self.reference()?, pos }
1273            }
1274            "ALTER" => {
1275                let mut pairs = Vec::new();
1276                loop {
1277                    let paragraph = self.proc_name()?;
1278                    self.expect_word("TO")?;
1279                    if self.accept_word("PROCEED") {
1280                        self.expect_word("TO")?;
1281                    }
1282                    pairs.push((paragraph, self.proc_name()?));
1283                    if !self.starts_ref() {
1284                        break;
1285                    }
1286                }
1287                Stmt::Alter { pairs, pos }
1288            }
1289            "ENTRY" => {
1290                let Some(Tok::Alnum(name)) = self.peek().cloned() else {
1291                    return Err(self.error("an alphanumeric literal naming the entry point"));
1292                };
1293                self.at += 1;
1294                let using = if self.accept_word("USING") { self.parameters()? } else { Vec::new() };
1295                Stmt::Entry { name: name.to_ascii_uppercase(), using, pos }
1296            }
1297            "EVALUATE" => self.evaluate(pos)?,
1298            "INITIATE" | "GENERATE" | "TERMINATE" | "SUPPRESS" => Stmt::Report(Box::new(self.report_statement(&verb, pos)?)),
1299            "GOBACK" => Stmt::Goback { pos },
1300            "STOP" => {
1301                self.expect_word("RUN")?;
1302                Stmt::StopRun { pos }
1303            }
1304            "CONTINUE" => Stmt::Continue,
1305            "EXIT" => match self.accept_any(&["PROGRAM", "PARAGRAPH", "SECTION", "PERFORM", "METHOD"]).as_deref() {
1306                Some("PROGRAM") => Stmt::ExitProgram { pos },
1307                Some("METHOD") => Stmt::ExitMethod { pos },
1308                Some("PARAGRAPH") => Stmt::Exit { kind: ExitKind::Paragraph, pos },
1309                Some("SECTION") => Stmt::Exit { kind: ExitKind::Section, pos },
1310                Some(_) if self.accept_word("CYCLE") => Stmt::Exit { kind: ExitKind::PerformCycle, pos },
1311                Some(_) => Stmt::Exit { kind: ExitKind::Perform, pos },
1312                None => Stmt::Exit { kind: ExitKind::Plain, pos },
1313            },
1314            other => return Err(Error::at(pos, format!("{other} is not a statement ironwork for COBOL supports yet"))),
1315        })
1316    }
1317
1318    fn size_error(&mut self) -> R<Option<SizeError>> {
1319        let [h] = self.on_phrases(&["ON", "SIZE"], &["END-COMPUTE", "END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"], |p| {
1320            p.accept_word("ON");
1321            p.expect_word("SIZE")?;
1322            p.expect_word("ERROR").map(|()| 0)
1323        })?;
1324        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() }))
1325    }
1326
1327    fn targets(&mut self) -> R<Vec<Target>> {
1328        let mut out = Vec::new();
1329        while self.starts_ref() {
1330            let r = self.reference()?;
1331            out.push(Target { r, rounded: self.accept_word("ROUNDED") });
1332        }
1333        if out.is_empty() {
1334            return Err(self.error("a receiving item"));
1335        }
1336        Ok(out)
1337    }
1338
1339    fn refs(&mut self) -> R<Vec<Ref>> {
1340        let mut out = Vec::new();
1341        while self.starts_ref() {
1342            out.push(self.reference()?);
1343        }
1344        if out.is_empty() {
1345            return Err(self.error("a data name"));
1346        }
1347        Ok(out)
1348    }
1349
1350    fn operands_until(&mut self, stops: &[&str]) -> R<Vec<Expr>> {
1351        let mut out = Vec::new();
1352        while self.starts_operand() && !self.word().is_some_and(|w| stops.contains(&w)) {
1353            out.push(Expr::Operand(self.operand()?));
1354        }
1355        if out.is_empty() {
1356            return Err(self.error("an operand"));
1357        }
1358        Ok(out)
1359    }
1360
1361    fn arith(&mut self, verb: &str, pos: Pos) -> R<Arith> {
1362        let sum = |mut es: Vec<Expr>| {
1363            let first = es.remove(0);
1364            es.into_iter().fold(first, |acc, e| Expr::Bin(Box::new(acc), BinOp::Add, Box::new(e)))
1365        };
1366        let of = |t: &Target| Expr::Operand(Operand::Ref(t.r.clone()));
1367        let bin = |a: Expr, op: BinOp, b: Expr| Expr::Bin(Box::new(a), op, Box::new(b));
1368        let mut remainder = None;
1369        let (verb, computations) = match verb {
1370            "ADD" => {
1371                let addends = sum(self.operands_until(&["TO", "GIVING"])?);
1372                if self.accept_word("TO") {
1373                    if self.words_ahead_include("GIVING") {
1374                        let to = sum(self.operands_until(&["GIVING"])?);
1375                        self.expect_word("GIVING")?;
1376                        let targets = self.targets()?;
1377                        let total = bin(addends, BinOp::Add, to);
1378                        (ArithVerb::Add, targets.into_iter().map(|t| (t, total.clone())).collect())
1379                    } else {
1380                        let targets = self.targets()?;
1381                        (ArithVerb::Add, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Add, addends.clone()))).collect())
1382                    }
1383                } else {
1384                    self.expect_word("GIVING")?;
1385                    let targets = self.targets()?;
1386                    (ArithVerb::Add, targets.into_iter().map(|t| (t, addends.clone())).collect())
1387                }
1388            }
1389            "SUBTRACT" => {
1390                let subtrahend = sum(self.operands_until(&["FROM"])?);
1391                self.expect_word("FROM")?;
1392                if self.words_ahead_include("GIVING") {
1393                    let minuend = sum(self.operands_until(&["GIVING"])?);
1394                    self.expect_word("GIVING")?;
1395                    let targets = self.targets()?;
1396                    let diff = bin(minuend, BinOp::Sub, subtrahend);
1397                    (ArithVerb::Subtract, targets.into_iter().map(|t| (t, diff.clone())).collect())
1398                } else {
1399                    let targets = self.targets()?;
1400                    (ArithVerb::Subtract, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Sub, subtrahend.clone()))).collect())
1401                }
1402            }
1403            "MULTIPLY" => {
1404                let a = Expr::Operand(self.operand()?);
1405                self.expect_word("BY")?;
1406                if self.words_ahead_include("GIVING") {
1407                    let b = Expr::Operand(self.operand()?);
1408                    self.expect_word("GIVING")?;
1409                    let targets = self.targets()?;
1410                    (ArithVerb::Multiply, targets.into_iter().map(|t| (t, bin(a.clone(), BinOp::Mul, b.clone()))).collect())
1411                } else {
1412                    let targets = self.targets()?;
1413                    (ArithVerb::Multiply, targets.into_iter().map(|t| (t.clone(), bin(a.clone(), BinOp::Mul, of(&t)))).collect())
1414                }
1415            }
1416            _ => {
1417                let first = Expr::Operand(self.operand()?);
1418                let into = match self.accept_any(&["INTO", "BY"]).as_deref() {
1419                    Some("INTO") => true,
1420                    Some(_) => false,
1421                    None => return Err(self.error("INTO or BY")),
1422                };
1423                if !into || self.words_ahead_include("GIVING") {
1424                    let second = Expr::Operand(self.operand()?);
1425                    let (dividend, divisor) = if into { (second, first) } else { (first, second) };
1426                    self.expect_word("GIVING")?;
1427                    let targets = self.targets()?;
1428                    if self.accept_word("REMAINDER") {
1429                        let r = self.reference()?;
1430                        remainder = Some((Target { r, rounded: false }, dividend.clone(), divisor.clone()));
1431                    }
1432                    (ArithVerb::Divide, targets.into_iter().map(|t| (t, bin(dividend.clone(), BinOp::Div, divisor.clone()))).collect())
1433                } else {
1434                    let targets = self.targets()?;
1435                    (ArithVerb::Divide, targets.into_iter().map(|t| (t.clone(), bin(of(&t), BinOp::Div, first.clone()))).collect())
1436                }
1437            }
1438        };
1439        let size_error = self.size_error()?;
1440        self.accept_any(&["END-ADD", "END-SUBTRACT", "END-MULTIPLY", "END-DIVIDE"]);
1441        Ok(Arith { verb, computations, remainder, size_error, pos })
1442    }
1443
1444    /// Whether `word` appears before the statement ends.
1445    fn words_ahead_include(&self, word: &str) -> bool {
1446        self.tokens[self.at..]
1447            .iter()
1448            .take_while(|t| t.tok != Tok::Period && !matches!(&t.tok, Tok::Word(w) if VERBS.contains(&w.as_str()) && w != word))
1449            .any(|t| matches!(&t.tok, Tok::Word(w) if w == word))
1450    }
1451
1452    fn perform(&mut self, pos: Pos) -> R<Stmt> {
1453        let named = self.word().is_some_and(|w| !VERBS.contains(&w) && !PHRASE_WORDS.contains(&w))
1454            && self.word_at(1) != Some("TIMES")
1455            && !matches!(self.peek(), Some(Tok::Number(_)));
1456        if named {
1457            let from = self.proc_name()?;
1458            let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.proc_name()?) } else { None };
1459            let repeat = self.repeat()?;
1460            return Ok(Stmt::PerformProc { from, thru, repeat, pos });
1461        }
1462        let repeat = self.repeat()?;
1463        if matches!(&repeat, Loop::Varying { after, .. } if !after.is_empty()) {
1464            return Err(Error::at(pos, "an inline PERFORM cannot have AFTER phrases: Enterprise COBOL takes them only when PERFORM names a procedure"));
1465        }
1466        let body = self.block(&["END-PERFORM"])?;
1467        self.expect_word("END-PERFORM")?;
1468        Ok(Stmt::PerformInline { body, repeat, pos })
1469    }
1470
1471    /// ON and NOT ON phrases, each opening with one of `starts`; `head` reads its words and picks the handlers it fills.
1472    fn on_phrases<const N: usize>(&mut self, starts: &[&str], ends: &[&str], head: impl Fn(&mut Self) -> R<usize>) -> R<[Handlers; N]> {
1473        self.phrases_opening(|p, i| p.word_at(i).is_some_and(|w| starts.contains(&w)), ends, head)
1474    }
1475
1476    /// ON and NOT ON phrases, each where `opens` finds one `i` words ahead.
1477    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]> {
1478        let stops = [&["NOT"][..], ends].concat();
1479        let mut handlers = std::array::from_fn(|_| Handlers::default());
1480        loop {
1481            let negated = self.is_word("NOT") && opens(self, 1);
1482            if !negated && !opens(self, 0) {
1483                return Ok(handlers);
1484            }
1485            self.at += usize::from(negated);
1486            let h = &mut handlers[head(self)?];
1487            let body = self.block(&stops)?;
1488            if negated { h.not_on = Some(body) } else { h.on = Some(body) }
1489        }
1490    }
1491
1492    /// INVALID KEY and NOT INVALID KEY phrases, then the scope terminator.
1493    fn invalid_key(&mut self, end: &str) -> R<Handlers> {
1494        let [h] = self.on_phrases(&["INVALID"], &[end], |p| {
1495            p.at += 1;
1496            p.accept_word("KEY");
1497            Ok(0)
1498        })?;
1499        self.accept_word(end);
1500        Ok(h)
1501    }
1502
1503    fn call(&mut self, pos: Pos) -> R<Call> {
1504        let target = self.operand()?;
1505        let mut using = Vec::new();
1506        if self.accept_word("USING") {
1507            let mut mode = ArgMode::Reference;
1508            loop {
1509                if self.accept_word("BY") {
1510                    mode = match self.accept_any(&["REFERENCE", "CONTENT", "VALUE"]).as_deref() {
1511                        Some("CONTENT") => ArgMode::Content,
1512                        Some("VALUE") => ArgMode::Value,
1513                        Some(_) => ArgMode::Reference,
1514                        None => return Err(self.error("REFERENCE, CONTENT or VALUE after BY")),
1515                    };
1516                } else if self.accept_word("OMITTED") {
1517                    using.push(Arg { mode, value: None });
1518                } else if self.starts_operand() {
1519                    using.push(Arg { mode, value: Some(self.operand()?) });
1520                } else {
1521                    break;
1522                }
1523            }
1524        }
1525        let returning = if self.accept_word("RETURNING") { Some(self.reference()?) } else { None };
1526        let [exception] = self.on_phrases(&["ON", "EXCEPTION", "OVERFLOW"], &["END-CALL"], |p| {
1527            p.accept_word("ON");
1528            p.accept_any(&["EXCEPTION", "OVERFLOW"]).map(|_| 0).ok_or_else(|| p.error("EXCEPTION or OVERFLOW"))
1529        })?;
1530        self.accept_word("END-CALL");
1531        Ok(Call { target, using, returning, on_exception: exception.on, not_on_exception: exception.not_on, pos })
1532    }
1533
1534    /// ON OVERFLOW and NOT ON OVERFLOW phrases, in either order.
1535    fn overflow(&mut self, end: &str) -> R<Handlers> {
1536        let [h] = self.on_phrases(&["ON", "OVERFLOW"], &[end], |p| {
1537            p.accept_word("ON");
1538            p.expect_word("OVERFLOW").map(|()| 0)
1539        })?;
1540        self.accept_word(end);
1541        Ok(h)
1542    }
1543
1544    fn string(&mut self, pos: Pos) -> R<StringStmt> {
1545        let mut sources = Vec::new();
1546        while !self.is_word("INTO") {
1547            let mut group = Vec::new();
1548            while self.starts_operand() && !self.is_word("DELIMITED") {
1549                group.push(self.operand()?);
1550            }
1551            if group.is_empty() {
1552                return Err(self.error("a sending item"));
1553            }
1554            let delimiter = if self.accept_word("DELIMITED") {
1555                self.accept_word("BY");
1556                if self.accept_word("SIZE") { Delimiter::Size } else { Delimiter::By(self.operand()?) }
1557            } else {
1558                Delimiter::Size
1559            };
1560            sources.extend(group.into_iter().map(|op| (op, delimiter.clone())));
1561        }
1562        self.expect_word("INTO")?;
1563        let into = self.reference()?;
1564        let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
1565            self.expect_word("POINTER")?;
1566            Some(self.reference()?)
1567        } else {
1568            None
1569        };
1570        let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-STRING")?;
1571        Ok(StringStmt { sources, into, pointer, on_overflow, not_on_overflow, pos })
1572    }
1573
1574    fn unstring(&mut self, pos: Pos) -> R<Unstring> {
1575        let source = self.reference()?;
1576        let mut delimiters = Vec::new();
1577        if self.accept_word("DELIMITED") {
1578            self.accept_word("BY");
1579            loop {
1580                let all = self.accept_word("ALL");
1581                delimiters.push((all, self.operand()?));
1582                if !self.accept_word("OR") {
1583                    break;
1584                }
1585            }
1586        }
1587        self.expect_word("INTO")?;
1588        let mut into = Vec::new();
1589        while self.starts_ref() {
1590            let target = self.reference()?;
1591            let delimiter_in = if self.accept_word("DELIMITER") {
1592                self.accept_word("IN");
1593                Some(self.reference()?)
1594            } else {
1595                None
1596            };
1597            let count_in = if self.accept_word("COUNT") {
1598                self.accept_word("IN");
1599                Some(self.reference()?)
1600            } else {
1601                None
1602            };
1603            into.push(UnstringInto { target, delimiter_in, count_in });
1604        }
1605        if into.is_empty() {
1606            return Err(self.error("a receiving item after INTO"));
1607        }
1608        let pointer = if self.accept_word("WITH") || self.is_word("POINTER") {
1609            self.expect_word("POINTER")?;
1610            Some(self.reference()?)
1611        } else {
1612            None
1613        };
1614        let tallying = if self.accept_word("TALLYING") {
1615            self.accept_word("IN");
1616            Some(self.reference()?)
1617        } else {
1618            None
1619        };
1620        let Handlers { on: on_overflow, not_on: not_on_overflow } = self.overflow("END-UNSTRING")?;
1621        Ok(Unstring { source, delimiters, into, pointer, tallying, on_overflow, not_on_overflow, pos })
1622    }
1623
1624    fn bounds(&mut self) -> R<Vec<Bound>> {
1625        let mut bounds = Vec::new();
1626        while let Some(side) = self.accept_any(&["BEFORE", "AFTER"]) {
1627            self.accept_word("INITIAL");
1628            bounds.push(Bound { after: side == "AFTER", value: self.operand()? });
1629        }
1630        Ok(bounds)
1631    }
1632
1633    fn inspect(&mut self, pos: Pos) -> R<Inspect> {
1634        let target = self.reference()?;
1635        let (mut tallying, mut replacing, mut converting) = (Vec::new(), Vec::new(), None);
1636        if self.accept_word("TALLYING") {
1637            while self.starts_ref() && self.word_at(1) == Some("FOR") || self.starts_ref() && !self.is_word("REPLACING") && self.tally_counter_ahead() {
1638                let counter = self.reference()?;
1639                self.expect_word("FOR")?;
1640                loop {
1641                    if self.accept_word("CHARACTERS") {
1642                        tallying.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
1643                    } else if let Some(mode) = self.accept_any(&["ALL", "LEADING"]) {
1644                        let mode = if mode == "ALL" { InspectMode::All } else { InspectMode::Leading };
1645                        loop {
1646                            let pattern = self.operand()?;
1647                            tallying.push(InspectPhrase { mode, pattern: Some(pattern), by: None, counter: Some(counter.clone()), bounds: self.bounds()? });
1648                            if !self.starts_operand() || self.word_at(1) == Some("FOR") || self.is_word("ALL") || self.is_word("LEADING") {
1649                                break;
1650                            }
1651                        }
1652                    } else {
1653                        break;
1654                    }
1655                }
1656            }
1657        }
1658        if self.accept_word("REPLACING") {
1659            loop {
1660                if self.accept_word("CHARACTERS") {
1661                    self.expect_word("BY")?;
1662                    let by = self.operand()?;
1663                    replacing.push(InspectPhrase { mode: InspectMode::Characters, pattern: None, by: Some(by), counter: None, bounds: self.bounds()? });
1664                } else if let Some(mode) = self.accept_any(&["ALL", "LEADING", "FIRST"]) {
1665                    let mode = match mode.as_str() {
1666                        "ALL" => InspectMode::All,
1667                        "LEADING" => InspectMode::Leading,
1668                        _ => InspectMode::First,
1669                    };
1670                    loop {
1671                        let pattern = self.operand()?;
1672                        self.expect_word("BY")?;
1673                        let by = self.operand()?;
1674                        replacing.push(InspectPhrase { mode, pattern: Some(pattern), by: Some(by), counter: None, bounds: self.bounds()? });
1675                        if !self.starts_operand() || self.is_word("ALL") || self.is_word("LEADING") || self.is_word("FIRST") {
1676                            break;
1677                        }
1678                    }
1679                } else {
1680                    break;
1681                }
1682            }
1683        }
1684        if self.accept_word("CONVERTING") {
1685            let from = self.operand()?;
1686            self.expect_word("TO")?;
1687            let to = self.operand()?;
1688            converting = Some((from, to, self.bounds()?));
1689        }
1690        if tallying.is_empty() && replacing.is_empty() && converting.is_none() {
1691            return Err(self.error("TALLYING, REPLACING or CONVERTING"));
1692        }
1693        Ok(Inspect { target, tallying, replacing, converting, pos })
1694    }
1695
1696    /// Whether a counter and FOR follow, past any subscripts on the counter.
1697    fn tally_counter_ahead(&self) -> bool {
1698        let mut i = self.at + 1;
1699        if self.tokens.get(i).map(|t| &t.tok) == Some(&Tok::LParen) {
1700            let mut depth = 0;
1701            while let Some(t) = self.tokens.get(i) {
1702                match t.tok {
1703                    Tok::LParen => depth += 1,
1704                    Tok::RParen => {
1705                        depth -= 1;
1706                        if depth == 0 {
1707                            i += 1;
1708                            break;
1709                        }
1710                    }
1711                    _ => {}
1712                }
1713                i += 1;
1714            }
1715        }
1716        matches!(self.tokens.get(i).map(|t| &t.tok), Some(Tok::Word(w)) if w == "FOR")
1717    }
1718
1719    fn search(&mut self, pos: Pos) -> R<Search> {
1720        let all = self.accept_word("ALL");
1721        let table = self.reference()?;
1722        let varying = if self.accept_word("VARYING") { Some(self.reference()?) } else { None };
1723        let at_end = if self.accept_word("AT") || self.is_word("END") {
1724            self.expect_word("END")?;
1725            Some(self.block(&["WHEN"])?)
1726        } else {
1727            None
1728        };
1729        let mut whens = Vec::new();
1730        while self.accept_word("WHEN") {
1731            let cond = self.cond()?;
1732            let body = self.block(&["WHEN", "END-SEARCH"])?;
1733            whens.push((cond, body));
1734        }
1735        if whens.is_empty() {
1736            return Err(self.error("WHEN"));
1737        }
1738        self.accept_word("END-SEARCH");
1739        Ok(Search { table, all, varying, at_end, whens, pos })
1740    }
1741
1742    fn set(&mut self) -> R<SetStmt> {
1743        if self.is_word("ENVIRONMENT") {
1744            return Err(Error::at(self.pos(), "SET ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's"));
1745        }
1746        if self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
1747            let mut targets = Vec::new();
1748            while self.is_word("ADDRESS") && self.word_at(1) == Some("OF") {
1749                self.at += 2;
1750                targets.push(self.reference()?);
1751            }
1752            self.expect_word("TO")?;
1753            return Ok(SetStmt::AddressOf { targets, value: self.operand()? });
1754        }
1755        let targets = self.refs()?;
1756        if self.accept_word("TO") {
1757            if self.accept_word("TRUE") {
1758                return Ok(SetStmt::ConditionTrue(targets));
1759            }
1760            if self.accept_word("FALSE") {
1761                return Ok(SetStmt::ConditionFalse(targets));
1762            }
1763            return Ok(SetStmt::To { targets, value: self.operand()? });
1764        }
1765        match self.accept_any(&["UP", "DOWN"]).as_deref() {
1766            Some(direction) => {
1767                self.expect_word("BY")?;
1768                Ok(SetStmt::UpDown { targets, down: direction == "DOWN", by: self.expr()? })
1769            }
1770            None => Err(self.error("TO, UP BY or DOWN BY")),
1771        }
1772    }
1773
1774    fn proc_name(&mut self) -> R<ProcName> {
1775        let name = self.name("a procedure name")?;
1776        let section = if self.accept_any(&["OF", "IN"]).is_some() { Some(self.name("a section name")?) } else { None };
1777        Ok(ProcName { name, section })
1778    }
1779
1780    fn evaluate(&mut self, pos: Pos) -> R<Stmt> {
1781        let mut subjects = vec![self.subject()?];
1782        while self.accept_word("ALSO") {
1783            subjects.push(self.subject()?);
1784        }
1785        let (mut whens, mut other) = (Vec::new(), Vec::new());
1786        while self.is_word("WHEN") {
1787            if self.word_at(1) == Some("OTHER") {
1788                self.at += 2;
1789                other = self.block(&["END-EVALUATE"])?;
1790                break;
1791            }
1792            let mut alternatives = Vec::new();
1793            while self.is_word("WHEN") && self.word_at(1) != Some("OTHER") {
1794                self.at += 1;
1795                let mut objects = Vec::new();
1796                for (k, subject) in subjects.iter().enumerate() {
1797                    if k > 0 {
1798                        self.expect_word("ALSO")?;
1799                    }
1800                    objects.push(self.object(subject)?);
1801                }
1802                alternatives.push(objects);
1803            }
1804            let body = self.block(&["WHEN", "END-EVALUATE"])?;
1805            whens.push(When { alternatives, body });
1806        }
1807        self.accept_word("END-EVALUATE");
1808        Ok(Stmt::Evaluate { subjects, whens, other, pos })
1809    }
1810
1811    fn subject(&mut self) -> R<Subject> {
1812        if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
1813            return Ok(Subject::Bool(b == "TRUE"));
1814        }
1815        let save = self.at;
1816        self.expr()?;
1817        let conditional = self.relop_ahead(0) || self.is_word("IS") || self.is_word("NOT")
1818            || self.word().is_some_and(|w| matches!(w, "NUMERIC" | "ALPHABETIC" | "POSITIVE" | "NEGATIVE" | "ZERO"));
1819        self.at = save;
1820        Ok(if conditional { Subject::Cond(self.cond()?) } else { Subject::Expr(self.expr()?) })
1821    }
1822
1823    fn object(&mut self, subject: &Subject) -> R<Object> {
1824        if self.accept_word("ANY") {
1825            return Ok(Object::Any);
1826        }
1827        if let Some(b) = self.accept_any(&["TRUE", "FALSE"]) {
1828            return Ok(Object::Bool(b == "TRUE"));
1829        }
1830        if !matches!(subject, Subject::Expr(_)) {
1831            return Ok(Object::Cond(self.cond()?));
1832        }
1833        let not = self.accept_word("NOT");
1834        let from = self.expr()?;
1835        let thru = if self.accept_any(&["THRU", "THROUGH"]).is_some() { Some(self.expr()?) } else { None };
1836        Ok(Object::Value { not, from, thru })
1837    }
1838
1839    fn repeat(&mut self) -> R<Loop> {
1840        let mut test_after = false;
1841        if self.accept_word("WITH") || self.is_word("TEST") {
1842            self.expect_word("TEST")?;
1843            test_after = self.accept_any(&["BEFORE", "AFTER"]).as_deref() == Some("AFTER");
1844        }
1845        if self.accept_word("UNTIL") {
1846            return Ok(Loop::Until { cond: self.cond()?, test_after });
1847        }
1848        if self.accept_word("VARYING") {
1849            let varying = Box::new(self.varying()?);
1850            let mut after = Vec::new();
1851            while self.is_word("AFTER") {
1852                if after.len() == 6 {
1853                    return Err(self.error("the end of the PERFORM: Enterprise COBOL takes at most six AFTER phrases"));
1854                }
1855                self.at += 1;
1856                after.push(self.varying()?);
1857            }
1858            return Ok(Loop::Varying { varying, after, test_after });
1859        }
1860        if self.starts_operand() && self.word_at(1) == Some("TIMES") || matches!(self.peek(), Some(Tok::Number(_))) {
1861            let count = self.expr()?;
1862            self.expect_word("TIMES")?;
1863            return Ok(Loop::Times(count));
1864        }
1865        Ok(Loop::Once)
1866    }
1867
1868    /// One VARYING or AFTER phrase, after its keyword.
1869    fn varying(&mut self) -> R<Varying> {
1870        let var = self.reference()?;
1871        self.expect_word("FROM")?;
1872        let from = self.expr()?;
1873        self.expect_word("BY")?;
1874        let by = self.expr()?;
1875        self.expect_word("UNTIL")?;
1876        Ok(Varying { var, from, by, until: self.cond()? })
1877    }
1878
1879    fn starts_ref(&self) -> bool {
1880        self.word().is_some_and(|w| !VERBS.contains(&w) && !PHRASE_WORDS.contains(&w) && figurative(w).is_none() && w != "FUNCTION")
1881            && !self.paragraph_header()
1882    }
1883
1884    fn starts_operand(&self) -> bool {
1885        match self.peek() {
1886            Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Number(_)) => true,
1887            Some(Tok::Word(w)) => {
1888                (figurative(w).is_some() || matches!(w.as_str(), "ALL" | "FUNCTION" | "LENGTH" | "ADDRESS" | "DFHRESP" | "DFHVALUE") || self.starts_ref()) && !self.paragraph_header()
1889            }
1890            _ => false,
1891        }
1892    }
1893
1894    fn operand(&mut self) -> R<Operand> {
1895        let pos = self.pos();
1896        match self.peek() {
1897            Some(Tok::Word(w)) if w == "FUNCTION" => {
1898                self.at += 1;
1899                let name = self.name("a function name")?;
1900                let (mut args, mut modifier) = (Vec::new(), None);
1901                if self.peek() == Some(&Tok::LParen) && !self.refmod_ahead() {
1902                    self.at += 1;
1903                    while !self.accept(&Tok::RParen) {
1904                        if let Some(m) = self.accept_any(&["LEADING", "TRAILING"]) {
1905                            modifier = Some(m);
1906                            continue;
1907                        }
1908                        args.push(self.expr()?);
1909                    }
1910                }
1911                let refmod = self.refmod()?;
1912                Ok(Operand::Function(FunctionCall { name, args, modifier, refmod, pos }))
1913            }
1914            Some(Tok::Word(w)) if w == "LENGTH" && self.word_at(1) == Some("OF") => {
1915                self.at += 2;
1916                Ok(Operand::LengthOf(self.reference()?))
1917            }
1918            Some(Tok::Word(w)) if w == "DFHRESP" && self.peek_at(1) == Some(&Tok::LParen) => {
1919                self.at += 2;
1920                let condition = self.name("a CICS condition")?;
1921                self.expect(&Tok::RParen, "')'")?;
1922                let code = crate::system::resp_code(&condition).ok_or_else(|| Error::at(pos, format!("DFHRESP({condition}): not a CICS condition ironwork for COBOL knows")))?;
1923                Ok(Operand::Literal(Literal::Number(code.to_string())))
1924            }
1925            Some(Tok::Word(w)) if w == "DFHVALUE" && self.peek_at(1) == Some(&Tok::LParen) => {
1926                self.at += 2;
1927                let name = self.name("a CVDA value")?;
1928                self.expect(&Tok::RParen, "')'")?;
1929                let value = rt::cics_tables::cvda(&name).ok_or_else(|| Error::at(pos, format!("DFHVALUE({name}): not a CVDA ironwork for COBOL knows")))?;
1930                Ok(Operand::Literal(Literal::Number(value.to_string())))
1931            }
1932            Some(Tok::Word(w)) if w == "ADDRESS" && self.word_at(1) == Some("OF") => {
1933                self.at += 2;
1934                Ok(Operand::AddressOf(self.reference()?))
1935            }
1936            Some(Tok::Word(w)) if figurative(w).is_some() || w == "ALL" => Ok(Operand::Literal(self.literal()?)),
1937            Some(Tok::Word(_)) => Ok(Operand::Ref(self.reference()?)),
1938            Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Number(_)) => Ok(Operand::Literal(self.literal()?)),
1939            _ => Err(self.error("an operand")),
1940        }
1941    }
1942
1943    /// Whether the parenthesis at the cursor opens a reference modification `(start:length)`.
1944    fn refmod_ahead(&self) -> bool {
1945        let mut depth = 0;
1946        for t in &self.tokens[self.at..] {
1947            match t.tok {
1948                Tok::LParen => depth += 1,
1949                Tok::RParen => {
1950                    depth -= 1;
1951                    if depth == 0 {
1952                        return false;
1953                    }
1954                }
1955                Tok::Colon if depth == 1 => return true,
1956                Tok::Period => return false,
1957                _ => {}
1958            }
1959        }
1960        false
1961    }
1962
1963    fn refmod(&mut self) -> R<Option<RefMod>> {
1964        if self.peek() != Some(&Tok::LParen) || !self.refmod_ahead() {
1965            return Ok(None);
1966        }
1967        self.at += 1;
1968        let start = Box::new(self.expr()?);
1969        self.expect(&Tok::Colon, "':'")?;
1970        let length = if self.peek() == Some(&Tok::RParen) { None } else { Some(Box::new(self.expr()?)) };
1971        self.expect(&Tok::RParen, "')'")?;
1972        Ok(Some(RefMod { start, length }))
1973    }
1974
1975    fn reference(&mut self) -> R<Ref> {
1976        let pos = self.pos();
1977        let name = self.name("a data name")?;
1978        let mut qualifiers = Vec::new();
1979        while self.accept_any(&["OF", "IN"]).is_some() {
1980            qualifiers.push(self.name("a qualifier")?);
1981        }
1982        let mut subscripts = Vec::new();
1983        if self.peek() == Some(&Tok::LParen) && !self.refmod_ahead() {
1984            self.at += 1;
1985            while !self.accept(&Tok::RParen) {
1986                subscripts.push(self.expr()?);
1987            }
1988        }
1989        let refmod = self.refmod()?;
1990        Ok(Ref { name, qualifiers, subscripts, refmod, pos })
1991    }
1992
1993    fn expr(&mut self) -> R<Expr> {
1994        let mut left = self.term()?;
1995        loop {
1996            let op = match self.peek() {
1997                Some(Tok::Plus) => BinOp::Add,
1998                Some(Tok::Minus) => BinOp::Sub,
1999                _ => return Ok(left),
2000            };
2001            self.at += 1;
2002            left = Expr::Bin(Box::new(left), op, Box::new(self.term()?));
2003        }
2004    }
2005
2006    fn term(&mut self) -> R<Expr> {
2007        let mut left = self.power()?;
2008        loop {
2009            let op = match self.peek() {
2010                Some(Tok::Star) => BinOp::Mul,
2011                Some(Tok::Slash) => BinOp::Div,
2012                _ => return Ok(left),
2013            };
2014            self.at += 1;
2015            left = Expr::Bin(Box::new(left), op, Box::new(self.power()?));
2016        }
2017    }
2018
2019    fn power(&mut self) -> R<Expr> {
2020        let mut left = self.unary()?;
2021        while self.accept(&Tok::Power) {
2022            left = Expr::Bin(Box::new(left), BinOp::Pow, Box::new(self.unary()?));
2023        }
2024        Ok(left)
2025    }
2026
2027    fn unary(&mut self) -> R<Expr> {
2028        if self.accept(&Tok::Minus) {
2029            return Ok(Expr::Neg(Box::new(self.unary()?)));
2030        }
2031        self.accept(&Tok::Plus);
2032        if self.accept(&Tok::LParen) {
2033            let e = self.expr()?;
2034            self.expect(&Tok::RParen, "')'")?;
2035            return Ok(e);
2036        }
2037        Ok(Expr::Operand(self.operand()?))
2038    }
2039
2040    fn cond(&mut self) -> R<Cond> {
2041        let mut last = None;
2042        self.or_cond(&mut last)
2043    }
2044
2045    fn or_cond(&mut self, last: &mut Option<(Expr, RelOp)>) -> R<Cond> {
2046        let mut left = self.and_cond(last)?;
2047        while self.accept_word("OR") {
2048            left = Cond::Or(Box::new(left), Box::new(self.and_cond(last)?));
2049        }
2050        Ok(left)
2051    }
2052
2053    fn and_cond(&mut self, last: &mut Option<(Expr, RelOp)>) -> R<Cond> {
2054        let mut left = self.not_cond(last)?;
2055        while self.accept_word("AND") {
2056            left = Cond::And(Box::new(left), Box::new(self.not_cond(last)?));
2057        }
2058        Ok(left)
2059    }
2060
2061    fn not_cond(&mut self, last: &mut Option<(Expr, RelOp)>) -> R<Cond> {
2062        if self.is_word("NOT") && !self.relop_ahead(1) {
2063            self.at += 1;
2064            return Ok(Cond::Not(Box::new(self.not_cond(last)?)));
2065        }
2066        self.primary_cond(last)
2067    }
2068
2069    fn relop_ahead(&self, ahead: usize) -> bool {
2070        matches!(self.peek_at(ahead), Some(Tok::Eq | Tok::Lt | Tok::Gt | Tok::Le | Tok::Ge))
2071            || matches!(self.word_at(ahead), Some("EQUAL" | "GREATER" | "LESS"))
2072    }
2073
2074    fn primary_cond(&mut self, last: &mut Option<(Expr, RelOp)>) -> R<Cond> {
2075        if self.peek() == Some(&Tok::LParen) {
2076            let save = self.at;
2077            self.at += 1;
2078            let mut inner_last = None;
2079            if let Ok(c) = self.or_cond(&mut inner_last)
2080                && self.accept(&Tok::RParen)
2081                && !matches!(self.peek(), Some(Tok::Plus | Tok::Minus | Tok::Star | Tok::Slash | Tok::Power))
2082                && !self.relop_ahead(0)
2083                && !self.is_word("IS")
2084            {
2085                return Ok(c);
2086            }
2087            self.at = save;
2088        }
2089        let left = self.expr()?;
2090        self.accept_word("IS");
2091        let negated = self.is_word("NOT") && {
2092            self.at += 1;
2093            true
2094        };
2095        let wrap = |c: Cond| if negated { Cond::Not(Box::new(c)) } else { c };
2096        if let Some(op) = self.relop()? {
2097            let right = self.expr()?;
2098            *last = Some((left.clone(), op));
2099            return Ok(wrap(Cond::Rel(left, op, right)));
2100        }
2101        if let Some(class) = self.accept_any(&["NUMERIC", "ALPHABETIC", "POSITIVE", "NEGATIVE", "ZERO"]) {
2102            let class = match class.as_str() {
2103                "NUMERIC" => Class::Numeric,
2104                "ALPHABETIC" => Class::Alphabetic,
2105                "POSITIVE" => Class::Positive,
2106                "NEGATIVE" => Class::Negative,
2107                _ => Class::Zero,
2108            };
2109            return Ok(wrap(Cond::Class(left, class)));
2110        }
2111        if negated {
2112            return Err(self.error("a relational operator or class after NOT"));
2113        }
2114        match (left, last.clone()) {
2115            (Expr::Operand(Operand::Ref(name)), Some((subject, op))) if self.abbreviation_context() => Ok(Cond::NameOrRel { subject, op, name }),
2116            (right, Some((subject, op))) if !matches!(&right, Expr::Operand(Operand::Ref(_))) => Ok(Cond::Rel(subject, op, right)),
2117            (Expr::Operand(Operand::Ref(r)), _) => Ok(Cond::Name(r)),
2118            (_, _) => Err(self.error("a relational operator")),
2119        }
2120    }
2121
2122    /// After AND or OR, an operand with no operator of its own continues an abbreviated relation.
2123    fn abbreviation_context(&self) -> bool {
2124        self.at > 1 && matches!(self.tokens.get(self.at.saturating_sub(2)).map(|t| &t.tok), Some(Tok::Word(w)) if w == "OR" || w == "AND")
2125    }
2126
2127    fn relop(&mut self) -> R<Option<RelOp>> {
2128        if self.peek() == Some(&Tok::Lt) && self.peek_at(1) == Some(&Tok::Gt) {
2129            return Err(Error::at(self.pos(), "<> is not an Enterprise COBOL relational operator: it writes NOT ="));
2130        }
2131        let op = match self.peek() {
2132            Some(Tok::Eq) => RelOp::Eq,
2133            Some(Tok::Lt) => RelOp::Lt,
2134            Some(Tok::Gt) => RelOp::Gt,
2135            Some(Tok::Le) => RelOp::Le,
2136            Some(Tok::Ge) => RelOp::Ge,
2137            Some(Tok::Word(w)) if w == "EQUAL" => {
2138                self.at += 1;
2139                self.accept_word("TO");
2140                return Ok(Some(RelOp::Eq));
2141            }
2142            Some(Tok::Word(w)) if w == "GREATER" || w == "LESS" => {
2143                let greater = w == "GREATER";
2144                self.at += 1;
2145                self.accept_word("THAN");
2146                let or_equal = self.accept_word("OR");
2147                if or_equal {
2148                    self.expect_word("EQUAL")?;
2149                    self.accept_word("TO");
2150                }
2151                return Ok(Some(match (greater, or_equal) {
2152                    (true, false) => RelOp::Gt,
2153                    (true, true) => RelOp::Ge,
2154                    (false, false) => RelOp::Lt,
2155                    (false, true) => RelOp::Le,
2156                }));
2157            }
2158            _ => return Ok(None),
2159        };
2160        self.at += 1;
2161        Ok(Some(op))
2162    }
2163}
2164
2165/// SQL host variables: `:NAME`, `:GROUP.NAME` and indicator variables, outside quoted strings.
2166fn host_variables(sql: &str, pos: Pos) -> Vec<Ref> {
2167    let chars: Vec<char> = sql.chars().collect();
2168    let (mut out, mut i, mut quote) = (Vec::new(), 0, None);
2169    while i < chars.len() {
2170        let c = chars[i];
2171        match quote {
2172            Some(q) if c == q => quote = None,
2173            Some(_) => {}
2174            None if c == '\'' || c == '"' => quote = Some(c),
2175            None if c == ':' && chars.get(i + 1).is_some_and(|n| n.is_ascii_alphanumeric()) => {
2176                let start = i + 1;
2177                let mut end = start;
2178                while end < chars.len() && (chars[end].is_ascii_alphanumeric() || matches!(chars[end], '-' | '_' | '.')) {
2179                    end += 1;
2180                }
2181                let path: String = chars[start..end].iter().collect::<String>().to_ascii_uppercase();
2182                let mut parts: Vec<String> = path.trim_end_matches('.').split('.').map(str::to_owned).collect();
2183                let name = parts.pop().unwrap_or_default();
2184                parts.reverse();
2185                out.push(Ref { name, qualifiers: parts, subscripts: Vec::new(), refmod: None, pos });
2186                i = end;
2187                continue;
2188            }
2189            None => {}
2190        }
2191        i += 1;
2192    }
2193    out
2194}
2195
2196/// CICS command words and options: `NAME` or `NAME(argument)`, the argument kept as written.
2197fn cics_options(body: &str) -> Vec<(String, Option<ExecArg>)> {
2198    let chars: Vec<char> = body.chars().collect();
2199    let (mut out, mut i) = (Vec::new(), 0);
2200    while i < chars.len() {
2201        if chars[i].is_whitespace() {
2202            i += 1;
2203            continue;
2204        }
2205        let start = i;
2206        while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '(' {
2207            i += 1;
2208        }
2209        let name: String = chars[start..i].iter().collect::<String>().to_ascii_uppercase();
2210        let mut j = i;
2211        while j < chars.len() && chars[j].is_whitespace() {
2212            j += 1;
2213        }
2214        if j < chars.len() && chars[j] == '(' {
2215            let (mut depth, mut quote, mut k) = (0, None, j);
2216            while k < chars.len() {
2217                match (quote, chars[k]) {
2218                    (Some(q), c) if c == q => quote = None,
2219                    (Some(_), _) => {}
2220                    (None, '\'' | '"') => quote = Some(chars[k]),
2221                    (None, '(') => depth += 1,
2222                    (None, ')') => {
2223                        depth -= 1;
2224                        if depth == 0 {
2225                            break;
2226                        }
2227                    }
2228                    _ => {}
2229                }
2230                k += 1;
2231            }
2232            let arg: String = chars[j + 1..k.min(chars.len())].iter().collect();
2233            out.push((name, Some(ExecArg::Text(arg.trim().to_owned()))));
2234            i = (k + 1).min(chars.len());
2235        } else {
2236            out.push((name, None));
2237        }
2238    }
2239    out
2240}
2241
2242/// An argument as the COBOL operand it names, through ironwork's own lexer and parser.
2243fn operand_of(text: &str, pos: Pos) -> Option<Operand> {
2244    let source = crate::source::Source { text: text.to_owned(), positions: vec![pos; text.chars().count()], options: Vec::new(), debugging: None };
2245    let tokens = crate::lexer::lex(&source).ok()?;
2246    let mut p = Parser { tokens: &tokens, at: 0, exec_declarations: Vec::new(), cics: false, sql: SqlState::default(), mnemonics: Vec::new(), debugging: false };
2247    let op = p.operand().ok()?;
2248    (p.at == tokens.len()).then_some(op)
2249}
2250
2251/// DATA DIVISION entries from the text of a system member.
2252fn system_entries(member: &str) -> R<Vec<DataEntry>> {
2253    system_text_entries(&crate::system::member(member).unwrap_or_default())
2254}
2255
2256fn system_text_entries(text: &str) -> R<Vec<DataEntry>> {
2257    let source = crate::source::read(text)?;
2258    let tokens = crate::lexer::lex(&source)?;
2259    Parser { tokens: &tokens, at: 0, exec_declarations: Vec::new(), cics: false, sql: SqlState::default(), mnemonics: Vec::new(), debugging: false }.data_entries()
2260}
2261
2262/// Words that begin a SELECT clause, and so end the one before.
2263const SELECT_CLAUSES: &[&str] = &[
2264    "ASSIGN", "ORGANIZATION", "ACCESS", "FILE", "STATUS", "RECORD", "ALTERNATE", "RELATIVE", "LINE", "SEQUENTIAL", "INDEXED", "RESERVE",
2265    "PADDING", "LOCK", "SHARING",
2266];
2267
2268/// Words that begin an FD clause, and so end the clause or the list of report names before them.
2269const FD_WORDS: &[&str] = &[
2270    "RECORDING", "RECORD", "BLOCK", "LABEL", "DATA", "VALUE", "CODE-SET", "LINAGE", "REPORT", "REPORTS", "IS", "EXTERNAL", "GLOBAL", "STYLE",
2271];
2272
2273fn is_clause_word(w: &str) -> bool {
2274    matches!(
2275        w,
2276        "PIC" | "PICTURE" | "USAGE" | "VALUE" | "VALUES" | "REDEFINES" | "OCCURS" | "SIGN" | "LEADING" | "TRAILING" | "JUSTIFIED"
2277            | "JUST" | "SYNC" | "SYNCHRONIZED" | "GLOBAL" | "EXTERNAL" | "BLANK"
2278    ) || usage_word(w).is_some()
2279}
2280
2281#[cfg(test)]
2282mod tests {
2283    use super::*;
2284
2285    fn program(body: &str) -> Program {
2286        let text = format!(
2287            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{body}"
2288        );
2289        crate::parse(&text).unwrap_or_else(|e| panic!("{e}"))
2290    }
2291
2292    #[test]
2293    fn dfhresp_and_dfhvalue_fold_to_the_numbers_in_ibms_tables() {
2294        let body = |operand: &str| format!("       01  A PIC S9(8) COMP.\n       PROCEDURE DIVISION.\n           IF A = {operand}\n               GOBACK\n           END-IF.\n");
2295        let folds_to = |operand: &str, n: i32| {
2296            let p = program(&body(operand));
2297            assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains(&format!("Number(\"{n}\")")), "{operand}");
2298        };
2299        folds_to("DFHRESP(NOTFINISHED)", 113);
2300        folds_to("DFHRESP(DSIDERR)", 12);
2301        folds_to("DFHRESP(FILENOTFOUND)", 12);
2302        assert_eq!(rt::cics_tables::cvda("ENABLED"), Some(23));
2303        folds_to("DFHVALUE(ENABLED)", 23);
2304        for (operand, message) in [("DFHRESP(NOSUCH)", "DFHRESP(NOSUCH): not a CICS condition"), ("DFHVALUE(NOSUCH)", "DFHVALUE(NOSUCH): not a CVDA")] {
2305            let err = crate::parse(&format!("       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{}", body(operand))).unwrap_err();
2306            assert!(err.to_string().contains(message), "{err}");
2307        }
2308    }
2309
2310    #[test]
2311    fn an_exec_cics_program_compares_with_dfhresp_notfinished() {
2312        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");
2313    }
2314
2315    #[test]
2316    fn data_entries_with_clauses_in_any_order() {
2317        let p = program(
2318            "       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",
2319        );
2320        let ws = &p.working_storage;
2321        assert_eq!(ws.len(), 5);
2322        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()))));
2323        assert_eq!(ws[2].redefines.as_deref(), Some("A"));
2324        assert_eq!((ws[3].name.as_deref(), &ws[3].value), (None, &Some(Literal::Figurative(Figurative::Space))));
2325        assert_eq!(ws[4].occurs, Some(3));
2326    }
2327
2328    #[test]
2329    fn paragraphs_and_nested_if() {
2330        let p = program(
2331            "       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",
2332        );
2333        assert_eq!(p.paragraphs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["MAIN-LINE", "P2"]);
2334        let Stmt::If { otherwise, .. } = &p.paragraphs[1].statements[0] else { panic!() };
2335        assert!(matches!(otherwise[0], Stmt::If { .. }));
2336    }
2337
2338    #[test]
2339    fn a_program_names_the_programs_it_directly_contains() {
2340        let program = |id: &str, inner: &str| format!("       IDENTIFICATION DIVISION.\n       PROGRAM-ID. {id}.\n       PROCEDURE DIVISION.\n           GOBACK.\n{inner}       END PROGRAM {id}.\n");
2341        let text = program("OUTER", &[program("A", &program("A1", "")), program("B", "")].concat());
2342        let all = crate::parse_all_with(&text, &Default::default()).unwrap_or_else(|e| panic!("{e}"));
2343        let contained: Vec<(&str, &[String])> = all.iter().map(|p| (p.id.as_str(), p.nested.as_slice())).collect();
2344        assert_eq!(contained, [("OUTER", &["A".to_owned(), "B".to_owned()][..]), ("A", &["A1".to_owned()][..]), ("A1", &[][..]), ("B", &[][..])]);
2345    }
2346
2347    #[test]
2348    fn inline_perform_varying_with_a_compound_condition() {
2349        let p = program(
2350            "       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",
2351        );
2352        let Stmt::PerformInline { repeat: Loop::Varying { varying, .. }, .. } = &p.paragraphs[0].statements[0] else { panic!() };
2353        assert!(matches!(varying.until, Cond::Or(..)));
2354    }
2355
2356    #[test]
2357    fn functions_reference_modification_and_length_of() {
2358        let p = program(
2359            "       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",
2360        );
2361        let Stmt::Compute { expr: Expr::Bin(left, BinOp::Sub, _), .. } = &p.paragraphs[0].statements[0] else { panic!() };
2362        let Expr::Operand(Operand::Function(f)) = left.as_ref() else { panic!() };
2363        let Expr::Operand(Operand::Ref(r)) = &f.args[0] else { panic!() };
2364        assert!(r.refmod.is_some());
2365        assert!(matches!(&p.paragraphs[0].statements[2], Stmt::Move { from: Operand::LengthOf(_), .. }));
2366    }
2367
2368    #[test]
2369    fn divide_giving_remainder_and_add_to() {
2370        let p = program(
2371            "       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",
2372        );
2373        let Stmt::Arith(a) = &p.paragraphs[0].statements[0] else { panic!() };
2374        assert_eq!(a.verb, ArithVerb::Divide);
2375        assert!(a.remainder.is_some());
2376        let Stmt::Arith(add) = &p.paragraphs[0].statements[1] else { panic!() };
2377        assert!(add.computations[0].0.rounded);
2378    }
2379
2380    #[test]
2381    fn abbreviated_combined_relation() {
2382        let p = program("       01  A PIC 9.\n       PROCEDURE DIVISION.\n           IF A = 1 OR 2 CONTINUE END-IF.\n");
2383        let Stmt::If { cond: Cond::Or(_, right), .. } = &p.paragraphs[0].statements[0] else { panic!() };
2384        assert!(matches!(right.as_ref(), Cond::Rel(_, RelOp::Eq, _)));
2385    }
2386
2387    #[test]
2388    fn indexed_select_clauses_and_keyed_statements() {
2389        let text = [
2390            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       ENVIRONMENT DIVISION.\n       INPUT-OUTPUT SECTION.\n       FILE-CONTROL.\n",
2391            "           SELECT K ASSIGN TO KDD ORGANIZATION IS INDEXED\n",
2392            "               ACCESS MODE IS DYNAMIC RECORD KEY IS K-ID\n",
2393            "               ALTERNATE KEY K-ALT WITH DUPLICATES.\n",
2394            "           SELECT N ASSIGN TO NDD STATUS N-FS N-VS ORGANIZATION\n",
2395            "               INDEXED FILE STATUS IS N-FS RECORD N-ID.\n",
2396            "       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",
2397            "       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",
2398            "       PROCEDURE DIVISION.\n",
2399            "           READ K NEXT RECORD AT END CONTINUE\n",
2400            "               NOT AT END CONTINUE END-READ\n",
2401            "           READ K KEY IS K-ALT INVALID KEY CONTINUE END-READ\n",
2402            "           START K KEY IS NOT LESS THAN K-ID\n",
2403            "               INVALID KEY CONTINUE NOT INVALID KEY CONTINUE\n",
2404            "           END-START\n",
2405            "           REWRITE K-REC INVALID KEY CONTINUE END-REWRITE\n",
2406            "           DELETE K RECORD END-DELETE.\n",
2407        ]
2408        .concat();
2409        let p = crate::parse(&text).unwrap_or_else(|e| panic!("{e}"));
2410        let f = &p.files[0];
2411        assert_eq!((f.organization, f.access), (Organization::Indexed, Access::Dynamic));
2412        assert_eq!(f.record_key.as_ref().map(|r| r.name.as_str()), Some("K-ID"));
2413        assert_eq!(f.alternate_keys.iter().map(|(r, d)| (r.name.as_str(), *d)).collect::<Vec<_>>(), [("K-ALT", true)]);
2414        let n = &p.files[1];
2415        assert_eq!((n.organization, n.record_key.as_ref().map(|r| r.name.as_str())), (Organization::Indexed, Some("N-ID")));
2416        let s = &p.paragraphs[0].statements;
2417        let Stmt::Read(r) = &s[0] else { panic!() };
2418        assert!(r.next && !r.previous && r.at_end.on.is_some() && r.at_end.not_on.is_some());
2419        let Stmt::Read(r) = &s[1] else { panic!() };
2420        assert!(r.key.is_some() && r.invalid.on.is_some() && !r.next);
2421        let Stmt::Start { key: Some((RelOp::Ge, _)), invalid, .. } = &s[2] else { panic!() };
2422        assert!(invalid.on.is_some() && invalid.not_on.is_some());
2423        assert!(matches!(&s[3], Stmt::Rewrite { invalid, .. } if invalid.on.is_some()));
2424        assert!(matches!(&s[4], Stmt::Delete { .. }));
2425    }
2426
2427    #[test]
2428    fn entry_alter_the_go_to_forms_and_section_priorities() {
2429        let p = program(
2430            "       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",
2431        );
2432        let priorities: Vec<(&str, u8)> = p.paragraphs.iter().map(|q| (q.name.as_str(), q.priority)).collect();
2433        assert_eq!(priorities, [("S", 50), ("P1", 50), ("P2", 50), ("P3", 50), ("T", 0)]);
2434        let s = &p.paragraphs[1].statements;
2435        assert!(matches!(&s[0], Stmt::Entry { name, using, .. } if name == "ALT" && using == &[Param { by_value: true, name: "L".into() }]));
2436        assert!(matches!(&s[2], Stmt::Alter { pairs, .. } if pairs.len() == 2 && pairs[1].0.name == "P3"));
2437        assert!(matches!(&s[3], Stmt::GoToDepending { targets, on, .. } if targets.len() == 2 && on.name == "D"));
2438        assert!(matches!(&p.paragraphs[2].statements[0], Stmt::GoTo { target: None, .. }));
2439        assert!(matches!(&p.paragraphs[3].statements[0], Stmt::GoTo { target: Some(t), .. } if t.name == "P1"));
2440    }
2441
2442    #[test]
2443    fn unsupported_statements_are_named() {
2444        let text = "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       PROCEDURE DIVISION.\n           MOVE CORRESPONDING A TO B.\n";
2445        assert!(crate::parse(text).unwrap_err().message.contains("MOVE CORRESPONDING"));
2446    }
2447
2448    #[test]
2449    fn advancing_mnemonic_names_reach_contained_programs_and_linage_is_noted() {
2450        let text = [
2451            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. OUTER.\n       ENVIRONMENT DIVISION.\n",
2452            "       CONFIGURATION SECTION.\n       SPECIAL-NAMES.\n           C01 IS TOP-OF-PAGE CSP NO-SPACE\n",
2453            "           AFP-5A IS PAGE-MODE UPSI-0 IS SWITCH-0 ON STATUS IS SW-ON.\n",
2454            "       INPUT-OUTPUT SECTION.\n       FILE-CONTROL.\n           SELECT P ASSIGN TO PDD.\n",
2455            "       DATA DIVISION.\n       FILE SECTION.\n       FD  P LINAGE IS 60.\n       01  P-REC PIC X.\n",
2456            "       PROCEDURE DIVISION.\n           WRITE P-REC AFTER TOP-OF-PAGE\n",
2457            "           WRITE P-REC BEFORE ADVANCING NO-SPACE\n           WRITE P-REC AFTER ADVANCING PAGE-COUNT LINES.\n",
2458            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. INNER.\n       PROCEDURE DIVISION.\n",
2459            "           WRITE P-REC AFTER ADVANCING PAGE-MODE.\n       END PROGRAM INNER.\n       END PROGRAM OUTER.\n",
2460        ]
2461        .concat();
2462        let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
2463        let (outer, inner) = (&programs[0], &programs[1]);
2464        let pairs = |p: &Program| p.environment.mnemonics.iter().map(|(n, e)| format!("{n}={e}")).collect::<Vec<_>>();
2465        assert_eq!(pairs(outer), ["TOP-OF-PAGE=C01", "NO-SPACE=CSP", "PAGE-MODE=AFP-5A"]);
2466        assert_eq!(outer.files[0].linage.as_ref().map(|l| &l.lines), Some(&LinageValue::Integer("60".into())));
2467        let advancing = |p: &Program, i: usize| match &p.paragraphs[0].statements[i] {
2468            Stmt::Write { advancing: Some(a), .. } => a.clone(),
2469            other => panic!("{other:?}"),
2470        };
2471        assert!(matches!(advancing(outer, 0), Advancing::Mnemonic { before: false, environment, .. } if environment == "C01"));
2472        assert!(matches!(advancing(outer, 1), Advancing::Mnemonic { before: true, environment, .. } if environment == "CSP"));
2473        assert!(matches!(advancing(outer, 2), Advancing::Lines { before: false, .. }));
2474        assert!(matches!(advancing(inner, 0), Advancing::Mnemonic { environment, .. } if environment == "AFP-5A"));
2475    }
2476
2477    #[test]
2478    fn exit_section_opening_a_paragraph_is_a_statement_not_a_section() {
2479        let p = program("       PROCEDURE DIVISION.\n       MAIN-LINE SECTION.\n       SKIPPED.\n           EXIT SECTION.\n       NEVER.\n           GOBACK.\n");
2480        let names: Vec<&str> = p.paragraphs.iter().map(|q| q.name.as_str()).collect();
2481        assert_eq!(names, ["MAIN-LINE", "SKIPPED", "NEVER"]);
2482        assert!(matches!(p.paragraphs[1].statements[..], [Stmt::Exit { kind: ExitKind::Section, .. }, Stmt::SentenceEnd]), "{:?}", p.paragraphs[1].statements);
2483    }
2484
2485    fn linage_program(fds: &str, procedure: &str) -> String {
2486        [
2487            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       ENVIRONMENT DIVISION.\n       INPUT-OUTPUT SECTION.\n       FILE-CONTROL.\n",
2488            "           SELECT P ASSIGN TO PDD.\n           SELECT S ASSIGN TO SDD.\n           SELECT Q ASSIGN TO QDD.\n",
2489            "       DATA DIVISION.\n       FILE SECTION.\n",
2490            fds,
2491            "       WORKING-STORAGE SECTION.\n       01  SIZES.\n           05 BODY PIC 99.\n       01  T-M PIC 9.\n       PROCEDURE DIVISION.\n",
2492            procedure,
2493        ]
2494        .concat()
2495    }
2496
2497    #[test]
2498    fn linage_phrases_come_in_any_order_and_an_sds_is_dropped() {
2499        let fds = [
2500            "       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",
2501            "       SD  S LINAGE 10.\n       01  S-REC PIC X.\n",
2502            "       FD  Q LABEL RECORDS STANDARD LINAGE 5 RECORDING MODE F.\n       01  Q-REC PIC X.\n",
2503        ]
2504        .concat();
2505        let p = crate::parse(&linage_program(&fds, "           GOBACK.\n")).unwrap_or_else(|e| panic!("{e}"));
2506        let l = p.files[0].linage.as_ref().unwrap();
2507        assert!(matches!(&l.lines, LinageValue::Data(r) if r.name == "BODY" && r.qualifiers == ["SIZES"]));
2508        assert_eq!((&l.footing, &l.bottom), (&Some(LinageValue::Integer("45".into())), &Some(LinageValue::Integer("6".into()))));
2509        assert!(matches!(&l.top, Some(LinageValue::Data(r)) if r.name == "T-M"));
2510        assert_eq!(p.files[1].linage, None);
2511        assert_eq!((p.files[2].linage.as_ref().map(|l| &l.lines), p.files[2].recording), (Some(&LinageValue::Integer("5".into())), Some('F')));
2512        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")] {
2513            let text = linage_program(&format!("       FD  P {fd}.\n       01  P-REC PIC X.\n"), "           GOBACK.\n");
2514            let e = crate::parse(&text).unwrap_err();
2515            assert!(e.message.contains(expected), "{fd}: {}", e.message);
2516        }
2517    }
2518
2519    #[test]
2520    fn end_of_page_phrases_leave_a_read_its_not_at_end() {
2521        let procedure = [
2522            "           READ Q AT END WRITE P-REC\n",
2523            "               NOT AT END WRITE P-REC AT EOP CONTINUE END-WRITE\n           END-READ\n",
2524            "           WRITE P-REC BEFORE ADVANCING 2 LINES END-OF-PAGE CONTINUE\n",
2525            "               NOT AT END-OF-PAGE CONTINUE\n           END-WRITE\n",
2526            "           WRITE P-REC INVALID KEY CONTINUE NOT EOP CONTINUE.\n",
2527        ]
2528        .concat();
2529        let fds = "       FD  P LINAGE 5.\n       01  P-REC PIC X.\n       FD  Q.\n       01  Q-REC PIC X.\n";
2530        let p = crate::parse(&linage_program(fds, &procedure)).unwrap_or_else(|e| panic!("{e}"));
2531        let s = &p.paragraphs[0].statements;
2532        let Stmt::Read(r) = &s[0] else { panic!("{:?}", s[0]) };
2533        let (Some(on), Some(not_on)) = (&r.at_end.on, &r.at_end.not_on) else { panic!("{r:?}") };
2534        assert!(matches!(&on[0], Stmt::Write { end_of_page, .. } if *end_of_page == Handlers::default()));
2535        assert!(matches!(&not_on[0], Stmt::Write { end_of_page, .. } if end_of_page.on.is_some() && end_of_page.not_on.is_none()));
2536        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()));
2537        assert!(matches!(&s[2], Stmt::Write { invalid, end_of_page, .. } if invalid.on.is_some() && end_of_page.not_on.is_some()));
2538    }
2539
2540    #[test]
2541    fn decimal_point_is_comma_reaches_contained_programs_and_other_currency_signs_are_refused() {
2542        let text = [
2543            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. OUTER.\n       ENVIRONMENT DIVISION.\n",
2544            "       CONFIGURATION SECTION.\n       SPECIAL-NAMES.\n           CURRENCY SIGN IS '$'\n           DECIMAL-POINT IS COMMA.\n",
2545            "       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n       01  A PIC 9V9 VALUE 1,5.\n",
2546            "       PROCEDURE DIVISION.\n           GOBACK.\n",
2547            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. INNER.\n       PROCEDURE DIVISION.\n           GOBACK.\n",
2548            "       END PROGRAM INNER.\n       END PROGRAM OUTER.\n",
2549        ]
2550        .concat();
2551        let programs = crate::parse_all_with(&text, &crate::copy::Libraries::default()).unwrap_or_else(|e| panic!("{e}"));
2552        assert!(programs.iter().all(|p| p.environment.decimal_point_comma));
2553        assert_eq!(programs[0].working_storage[0].value, Some(Literal::Number("1.5".into())));
2554        let other = text.replace("'$'", "'W'");
2555        assert!(crate::parse(&other).unwrap_err().message.contains("CURRENCY SIGN"));
2556    }
2557
2558    #[test]
2559    fn renames_and_condition_names_with_a_false_value() {
2560        let p = program(concat!(
2561            "       01  R.\n           05 A PIC X.\n           05 B PIC X.\n              88 B-ON VALUE 'Y' FALSE 'N'.\n",
2562            "              88 B-OFF VALUES 'N' 'X' WHEN SET TO FALSE IS SPACE.\n",
2563            "       66  AB RENAMES A THRU B.\n       66  BB RENAMES B OF R.\n",
2564            "       PROCEDURE DIVISION.\n           SET B-ON B-OFF TO FALSE.\n",
2565        ));
2566        let ws = &p.working_storage;
2567        assert_eq!((ws[3].false_value.as_ref(), ws[3].condition_values.len()), (Some(&Literal::Alnum("N".into())), 1));
2568        assert_eq!((ws[4].false_value.as_ref(), ws[4].condition_values.len()), (Some(&Literal::Figurative(Figurative::Space)), 2));
2569        let (first, last) = ws[5].renames.as_ref().unwrap();
2570        assert_eq!((ws[5].level, first.name.as_str(), last.as_ref().map(|r| r.name.as_str())), (66, "A", Some("B")));
2571        assert_eq!(ws[6].renames.as_ref().unwrap().0.qualifiers, ["R"]);
2572        assert!(matches!(&p.paragraphs[0].statements[0], Stmt::Set { set: SetStmt::ConditionFalse(t), .. } if t.len() == 2));
2573    }
2574}