Skip to main content

ironwork_compile/
lib.rs

1//! ironwork for COBOL, the compiler: a parsed program checked against IBM's rules, with its
2//! WORKING-STORAGE laid out as IBM lays it out, ready for the interpreter or for lowering.
3
4pub mod any_length;
5pub mod arith;
6pub mod cics_bind;
7mod crt_status;
8mod classes;
9pub mod constructs;
10pub mod collating;
11mod corresponding;
12pub use corresponding::is_alphabetic;
13pub use operands::as_characters;
14pub mod declaratives;
15pub mod facts;
16pub mod function;
17mod initcheck;
18pub mod layout;
19pub mod linage;
20pub mod lower;
21pub mod markup;
22pub mod numcheck;
23mod omitted;
24pub mod oo;
25mod operands;
26pub mod picture;
27pub mod printer;
28pub mod report;
29mod registers;
30mod reserved;
31mod scope;
32mod screens;
33pub mod sort;
34pub mod sql;
35pub mod statements;
36mod switches;
37pub mod values;
38
39use layout::Layout;
40use numeric::{Options, Vlr};
41use rt::lir::{CompileTime, TimeSource};
42use rt::storage::literal_fixed;
43use syntax::ast::*;
44use syntax::messages::{IWC0003, IWC0004, Message};
45use syntax::{Error, Pos, Severity};
46
47pub struct Compiled {
48    pub program: Program,
49    /// FUNCTION WHEN-COMPILED's time.
50    pub when_compiled: CompileTime,
51    pub layout: Layout,
52    pub options: Options,
53    pub ssrange: bool,
54    pub report_writer: report::Writer,
55    /// The PROGRAM COLLATING SEQUENCE, or EBCDIC.
56    pub collating: collating::Sequence,
57    /// Each file's printer control character, when it is a print file.
58    pub carriage: Vec<Option<printer::Carriage>>,
59    /// The messages of a program that compiled, none of them severe enough to stop its object code.
60    pub diagnostics: Vec<Error>,
61    /// The program's ENTRY statements, in source order.
62    pub entries: Vec<EntryPoint>,
63    /// Where each EXCEPTION/ERROR and debugging procedure runs.
64    pub declaratives: declaratives::Table,
65    /// The user-defined functions the program may invoke.
66    pub functions: Vec<function::Udf>,
67}
68
69/// An alternate entry point: a CALL of `name` begins at statement `statement` of paragraph
70/// `paragraph`, the one after the ENTRY statement, with `using` addressing LINKAGE.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct EntryPoint {
73    pub name: String,
74    pub paragraph: usize,
75    pub statement: usize,
76    pub using: Vec<Param>,
77    pub pos: Pos,
78}
79
80/// The ENTRY statements of a program, each a sentence of its own paragraph's.
81pub fn entry_points(program: &Program) -> Vec<EntryPoint> {
82    let mut out = Vec::new();
83    for (paragraph, p) in program.paragraphs.iter().enumerate() {
84        for (k, s) in p.statements.iter().enumerate() {
85            if let Stmt::Entry { name, using, pos } = s {
86                out.push(EntryPoint { name: name.clone(), paragraph, statement: k + 1, using: using.clone(), pos: *pos });
87            }
88        }
89    }
90    out
91}
92
93/// Whether file k's records vary in length: RECORDING MODE V, RECORD IS VARYING, a RECORD clause
94/// with two bounds, or, with neither RECORDING MODE nor RECORD, level-01 records of different
95/// lengths or with an OCCURS DEPENDING ON table, an SD's as well (Language Reference SC27-8713-03,
96/// p. 191; Programming Guide SC27-8714-03, pp. 227-228).
97pub fn variable_records(file: &FileDecl, layout: &Layout, k: usize) -> bool {
98    let undeclared = file.recording.is_none() && file.record_min.is_none() && file.record_max.is_none() && !file.record_varying;
99    file.recording == Some('V') || file.record_varying || file.record_min != file.record_max || undeclared && layout.record_lengths[k].is_some_and(|(shortest, longest)| shortest != longest)
100}
101
102/// The shortest and longest variable-length record a READ of file k takes without a record length
103/// conflict: under VLR(STANDARD) its level-01 records', under VLR(COMPAT) its RECORD IS VARYING
104/// clause's, where a bound the clause leaves out is the level-01 records' (Programming Guide
105/// SC27-8714-03, pp. 422-424; Language Reference SC27-8713-03, pp. 187, 300, 431). See
106/// [`numeric::assumptions::VLR_WITHOUT_VARYING`] and [`numeric::assumptions::VLR_RECORDS_CHECKED`].
107pub fn read_lengths(file: &FileDecl, layout: &Layout, k: usize, vlr: Vlr) -> (u32, u32) {
108    match vlr {
109        Vlr::Compat if file.record_varying => varying_lengths(file, layout, k),
110        _ => record_lengths(layout, k),
111    }
112}
113
114/// The shortest and longest record RECORD IS VARYING allows file k, a bound it leaves out being the
115/// level-01 records' (Language Reference SC27-8713-03, p. 187).
116pub fn varying_lengths(file: &FileDecl, layout: &Layout, k: usize) -> (u32, u32) {
117    let (shortest, longest) = record_lengths(layout, k);
118    (file.record_min.unwrap_or(shortest), file.record_max.unwrap_or(longest))
119}
120
121fn record_lengths(layout: &Layout, k: usize) -> (u32, u32) {
122    layout.record_lengths[k].unwrap_or((0, layout.file_areas[k].1))
123}
124
125const FUNCTIONS: &[&str] = rt::intrinsic::FIRST;
126
127pub const NUMERIC_FUNCTION_MOVED: &str = "an integer or numeric function as a MOVE's sender (GnuCOBOL; Enterprise COBOL takes one only where an arithmetic expression can be)";
128pub const INTO_WITHOUT_COLON: &str = "an INTO name written without its colon (Db2 13 for z/OS requires the colon before every host variable)";
129
130/// Checks and lays out a parsed program. `flags` are this compiler's own, such as `-silent`. A
131/// program is refused, with every message, when one stops its object code: under IBM's default
132/// NOCOMPILE(S) one that is S or U, from W under `-warnings-block`, or as a CBL or PROCESS card's
133/// COMPILE or NOCOMPILE says; otherwise its messages are [`Compiled::diagnostics`].
134pub fn compile(program: Program, flags: &[String]) -> Result<Compiled, Vec<Error>> {
135    let at = compile_time().map_err(|message| vec![syntax::messages::IWO0004.at(Pos::default(), message)])?;
136    compile_at(program, flags, at)
137}
138
139/// Compiles as [`compile`] does, WHEN-COMPILED giving `at`.
140pub fn compile_at(program: Program, flags: &[String], at: CompileTime) -> Result<Compiled, Vec<Error>> {
141    if program.oo.as_ref().is_some_and(|o| o.class().is_some()) {
142        return oo::compile_class_definition(program, flags, at);
143    }
144    compile_program(program, flags, true, at)
145}
146
147/// When a compile happens: SOURCE_DATE_EPOCH's seconds when the build sets it, the
148/// reproducible-builds convention, and the clock otherwise.
149pub fn compile_time() -> Result<CompileTime, String> {
150    let clock = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
151    compile_time_from(std::env::var_os("SOURCE_DATE_EPOCH").as_deref(), clock)
152}
153
154fn compile_time_from(epoch: Option<&std::ffi::OsStr>, clock: std::time::Duration) -> Result<CompileTime, String> {
155    let Some(epoch) = epoch else {
156        return Ok(CompileTime { seconds: clock.as_secs() as i64, hundredths: clock.subsec_millis() / 10, source: TimeSource::Clock });
157    };
158    let text = epoch.to_string_lossy();
159    match text.parse::<i64>() {
160        Ok(seconds) if text.bytes().all(|b| b.is_ascii_digit()) && seconds <= CompileTime::LATEST => Ok(CompileTime { seconds, hundredths: 0, source: TimeSource::SourceDateEpoch }),
161        _ => Err(format!("SOURCE_DATE_EPOCH={text}: not a whole number of seconds from 0 to {}", CompileTime::LATEST)),
162    }
163}
164
165/// `whole` is false for the parts a class definition is compiled into, which IBM's rules for
166/// compiler options do not apply to one by one.
167pub(crate) fn compile_program(mut program: Program, flags: &[String], whole: bool, when_compiled: CompileTime) -> Result<Compiled, Vec<Error>> {
168    let mut errors = std::mem::take(&mut program.messages);
169    reserved::check(&program, &mut errors);
170    let mut declared = program.working_storage.len();
171    let mut program = declaratives::with_debug_item(markup::with_special_registers(sort::with_special_registers(registers::with_when_compiled(program, when_compiled))));
172    switches::declare(&mut program, &mut errors);
173    qualify_in_own_section(&mut program);
174    let mut options = Options::default();
175    let mut ssrange = false;
176    // The flags first, as the compiler's invocation, then the cards, which outrank it (Programming
177    // Guide SC27-8714-03, p. 273).
178    for flag in flags {
179        if let Err(e) = options.apply_flag(flag) {
180            errors.push(syntax::messages::IWO0005.at(Pos::default(), e.to_string()));
181        }
182    }
183    for option in &program.options {
184        if let Some(on) = numeric::options::switch(option, "SSRANGE") {
185            ssrange = on;
186        }
187        if let Err(e) = options.apply(option) {
188            errors.push(syntax::messages::IWO0001.at(Pos::default(), format!("CBL {option}: {e}")).graded(option_severity(&e)));
189        }
190    }
191    if options.compliance == numeric::Compliance::Extended {
192        let inserted = top_level_tables(&mut program.working_storage, &mut errors);
193        declared += inserted.iter().filter(|&&at| at < declared).count();
194        top_level_tables(&mut program.local_storage, &mut errors);
195        let Program { working_storage, local_storage, linkage, files, .. } = &mut program;
196        for entries in [working_storage, local_storage, linkage].into_iter().chain(files.iter_mut().map(|f| &mut f.records)) {
197            renames_inside_records(entries, &mut errors);
198        }
199        declared = based_items(&mut program, declared, &mut errors);
200    }
201    screens::expand(&mut program, options.compliance == numeric::Compliance::Extended, &mut errors);
202    let page = options.code_page();
203    if let Err((message, m)) = syntax::parser::decode_currency(&mut program.environment, |bytes| page.decode(bytes)) {
204        errors.push(message.at(Pos::default(), m));
205    }
206    default_currency(&mut program, &mut options, &mut errors);
207    national_symbols(&mut program, &mut options, &mut errors);
208    if options.intdate == numeric::IntDate::Lilian {
209        program.paragraphs.iter_mut().for_each(|p| ceecbldy_to_ceedays(&mut p.statements, &mut errors));
210    }
211    for (name, alphabet) in &program.environment.alphabets {
212        if program.environment.collating_sequence.as_ref() != Some(name)
213            && let Err((message, m)) = collating::Sequence::of(alphabet, options.code_page(), options.quote)
214        {
215            errors.push(message.at(Pos::default(), format!("ALPHABET {name}: {m}")));
216        }
217    }
218    let collating = collating::Sequence::program(&program.environment, options.code_page(), options.quote).unwrap_or_else(|(message, m)| {
219        errors.push(message.at(Pos::default(), m));
220        let mut native = collating::Sequence::native();
221        native.quote = options.quote;
222        native
223    });
224    digit_limits(&program, options.arith, &mut errors);
225    binary_chars(&program, &options, &mut errors);
226    forever_names(&program, &mut errors);
227    micro_focus_binaries(&program, &options, &mut errors);
228    let drafts = report::prepare(&mut program, options.adv, options.qualify, &mut errors);
229    let linage_counters = linage::add_counters(&mut program, options.qualify);
230    if whole {
231        oo::option_rules(&program, &options, &mut errors);
232    }
233    options.initial &= !options.thread;
234    if whole && program.oo.as_deref().and_then(Oo::method).is_none() {
235        program.initial |= options.initial;
236    }
237    scope::rules(&program, &mut errors);
238    if whole {
239        program_names(&program, &options, &mut errors);
240    }
241    let inherited = scope::inherit(&mut program);
242    let own_linkage = scope::own_linkage(&program);
243    omitted::rewrite(&mut program, &inherited.entries, options.compliance == numeric::Compliance::Extended, &mut errors);
244    any_length::rewrite(&mut program, &mut errors);
245    if options.compliance == numeric::Compliance::Extended {
246        crt_status::rewrite(&mut program, &inherited.entries);
247    }
248    declare_assign_items(&mut program, &inherited.entries);
249    let linkage: Vec<DataEntry> = program.linkage.iter().chain(&inherited.entries).cloned().collect();
250    let files: Vec<(&[DataEntry], Option<u32>)> = program.files.iter().map(|f| (f.records.as_slice(), f.record_max)).collect();
251    let shared = layout::record_area_owners(&program.files, &program.environment).unwrap_or_else(|e| {
252        errors.push(e);
253        (0..files.len()).collect()
254    });
255    let mut layout = match layout::build(&program.working_storage, &files, &shared, &linkage, program.linkage.len(), &program.local_storage, crate::picture::Notation::of(&program.environment), options.qualify, options.parmcheck.map(|p| (declared, p.bytes.into()))) {
256        Ok(l) => l,
257        Err(e) => {
258            errors.push(e);
259            return Err(errors.into_iter().map(|e| e.in_files(&program.sources)).collect());
260        }
261    };
262    scope::bind(&mut layout, own_linkage, inherited, &program.files);
263    layout.classes = classes::sets(&program.environment.classes, page, &mut errors);
264    let counter_item = |entry: usize| program.working_storage[..entry].iter().filter(|e| e.level != 88).count();
265    layout.name_files(&program.files, linage_counters.iter().map(|c| c.map(counter_item)).collect());
266    assign_items(&mut program, &layout, &options, &mut errors);
267    corresponding::expand(&mut program, &layout, &mut errors);
268    condition_subjects(&mut program, &layout);
269    dbcs_values(&layout, &mut errors);
270    numeric_values(&layout, &mut errors);
271    edited_values(&layout, options.compliance, &mut errors);
272    for item in &layout.items {
273        if let Some(object) = &item.depending_on {
274            match layout.resolve(&object.name, &object.qualifiers, object.pos) {
275                Ok(layout::Resolved::Item(i)) if !layout.items[i].moved_by.is_empty() => errors.push(syntax::messages::IWC0048.at(
276                    object.pos,
277                    format!("OCCURS DEPENDING ON {}: the object cannot follow an OCCURS DEPENDING ON table in its record", object.name),
278                )),
279                Ok(layout::Resolved::Item(i)) if layout.items[i].kind.is_numeric() => {}
280                Ok(_) => errors.push(syntax::messages::IWC0049.at(object.pos, format!("OCCURS DEPENDING ON {}: not a numeric data item", object.name))),
281                Err(e) => errors.push(e),
282            }
283        }
284    }
285    let is_record = |name: &str| layout.linkage_roots.iter().enumerate().any(|(o, &i)| layout.is_argument(o) && layout.items[i].name.as_deref() == Some(name));
286    for param in &program.using {
287        if !is_record(&param.name) {
288            errors.push(syntax::messages::IWC0050.at(Pos::default(), format!("PROCEDURE DIVISION USING {}: not an 01 or 77 item of the LINKAGE SECTION", param.name)));
289        }
290    }
291    // Language Reference SC27-8713-03, pp. 262-263; a method's and a function's are checked with their signatures.
292    if let Some(name) = &program.returning
293        && program.function.is_none()
294        && program.oo.as_deref().and_then(Oo::method).is_none()
295        && !is_record(name)
296    {
297        errors.push(syntax::messages::IWC0051.at(Pos::default(), format!("PROCEDURE DIVISION RETURNING {name}: not an 01 or 77 item of the LINKAGE SECTION")));
298    }
299    let report_writer = report::resolve(&program, &layout, drafts, &mut errors);
300    let carriage = printer::carriages(&program, &layout, options.adv);
301    let declaratives = declaratives::resolve(&program, &layout, &options, &mut errors);
302    let debugging = declaratives::debugging_sections(&program);
303    let functions = function::functions(&program, options.qualify, &mut errors);
304    let alphabetic: Vec<Pos> = [&program.working_storage, &program.local_storage, &program.linkage]
305        .into_iter()
306        .flatten()
307        .chain(program.files.iter().flat_map(|f| &f.records))
308        .filter(|e| e.picture.as_deref().is_some_and(is_alphabetic))
309        .map(|e| e.pos)
310        .collect();
311    let mut check = Check {
312        layout: &layout,
313        program: &program,
314        errors: &mut errors,
315        debugging: false,
316        max_digits: options.arith.max_picture_digits(),
317        inline_performs: 0,
318        functions: Some(&functions),
319        alphabetic: &alphabetic,
320        at: Pos::default(),
321        paragraph: 0,
322        extended: options.compliance == numeric::Compliance::Extended,
323        environment_named: None,
324    };
325    for k in 0..program.files.len() {
326        check.file_keys(k);
327        check.record_depending(k);
328        check.label_records(k);
329        check.recording_mode(k);
330        check.passwords(k);
331        linage::check_file(check.program, check.layout, k, check.errors);
332    }
333    for block in &program.exec_declarations {
334        check.exec_block(block);
335    }
336    for (i, p) in program.paragraphs.iter().enumerate() {
337        check.debugging = debugging.iter().any(|&(first, last)| (first..=last).contains(&i));
338        check.paragraph = i;
339        check.statements(&p.statements);
340    }
341    let entries = entry_points(&program);
342    procedure_rules(&program, &layout, &entries, &options, &mut errors);
343    if whole && !program.oo.as_deref().is_some_and(|o| o.method().is_some()) && !program.is_prototype() {
344        program_end(&program, &options, &mut errors);
345    }
346    oo::check(&layout, &program, &mut errors);
347    if let Some(mode) = options.initcheck {
348        errors.extend(initcheck::check(&program, &layout, declared, mode));
349    }
350    scope::check(&program, &layout, &mut errors);
351    layout.numcheck = numcheck::facts(&program, &layout, declared, &options, &collating, &mut errors);
352    let errors: Vec<Error> = errors.into_iter().map(|e| e.in_files(&program.sources)).collect();
353    if refused(&errors, &options) {
354        Err(errors)
355    } else {
356        Ok(Compiled { program, when_compiled, layout, options, ssrange, report_writer, collating, carriage, diagnostics: errors, entries, declaratives, functions })
357    }
358}
359
360/// Which files take their name from a data item at each OPEN. Under `--compliance extended` an
361/// ASSIGN name that is an alphanumeric or group item's is one, with IWX0007-W, and DYNAMIC or
362/// USING must name one; under strict a name stays the DD name it is in Enterprise COBOL and
363/// DYNAMIC and USING are refused (C361). A file SORT or MERGE reads or writes is refused one, as
364/// the sort opens its files by their DD names.
365/// Micro Focus's ASSIGN TO DISK name where neither the program nor a program containing it declares
366/// the name: a 4,095-byte alphanumeric item of WORKING-STORAGE, as cobc 3.2 declares it under
367/// `-std=mf` (assumption C482).
368fn declare_assign_items(program: &mut Program, inherited: &[DataEntry]) {
369    let mut missing: Vec<(String, Pos)> = Vec::new();
370    for a in program.files.iter().filter_map(|f| f.assign_item.as_ref()).filter(|a| a.declared_if_missing) {
371        let name = &a.reference.name;
372        let declared = program.working_storage.iter().chain(&program.local_storage).chain(&program.linkage).chain(inherited).chain(program.files.iter().flat_map(|f| &f.records)).any(|e| e.name.as_ref() == Some(name));
373        if !declared && !missing.iter().any(|(m, _)| m == name) {
374            missing.push((name.clone(), a.reference.pos));
375        }
376    }
377    for (name, pos) in missing {
378        program.working_storage.push(report::entry(1, Some(name), Some("X(4095)".into()), None, pos));
379    }
380}
381
382fn assign_items(program: &mut Program, layout: &layout::Layout, options: &Options, errors: &mut Vec<Error>) {
383    let extended = options.compliance == numeric::Compliance::Extended;
384    let mut all = Vec::new();
385    program.paragraphs.iter().for_each(|p| inner_statements(&p.statements, &mut all));
386    let sorted: Vec<&str> = all
387        .iter()
388        .filter_map(|s| match s {
389            Stmt::Sorting(sorting) => match sorting.as_ref() {
390                Sorting::Sort(s) => Some(s),
391                _ => None,
392            },
393            _ => None,
394        })
395        .flat_map(|s| [&s.input, &s.output])
396        .filter_map(|io| if let Some(SortIo::Files(names)) = io { Some(names) } else { None })
397        .flatten()
398        .map(String::as_str)
399        .collect();
400    let mut kept = Vec::new();
401    for (k, f) in program.files.iter().enumerate() {
402        let Some(a) = &f.assign_item else { continue };
403        let (name, pos) = (&a.reference.name, a.reference.pos);
404        if !extended {
405            if a.explicit {
406                errors.push(syntax::messages::IWC0052.at(pos, format!("ASSIGN USING or DYNAMIC {name}: a Micro Focus and GnuCOBOL form; --compliance extended reads it")));
407            }
408            continue;
409        }
410        let item = match layout.resolve(name, &a.reference.qualifiers, pos) {
411            Ok(layout::Resolved::Item(i)) => i,
412            Ok(layout::Resolved::Condition(_)) | Err(_) if a.explicit => {
413                errors.push(syntax::messages::IWC0053.at(pos, format!("ASSIGN {name}: not a data item")));
414                continue;
415            }
416            _ => continue,
417        };
418        if !matches!(layout.items[item].kind, rt::storage::Kind::Group | rt::storage::Kind::Alnum { .. }) {
419            errors.push(syntax::messages::IWC0054.at(pos, format!("ASSIGN {name}: the item holding the file's name must be alphanumeric or a group")));
420        } else if f.sort || sorted.contains(&f.name.as_str()) {
421            errors.push(syntax::messages::IWR0022.at(pos, format!("ASSIGN {name}: a file SORT or MERGE reads, writes or describes taking its name from a data item is not supported yet")));
422        } else {
423            errors.push(syntax::messages::IWX0007.at(pos, format!("{}: each OPEN of {} takes its DD name from {name}", syntax::extended::ASSIGN_ITEM, f.name)));
424            kept.push(k);
425        }
426    }
427    for (k, f) in program.files.iter_mut().enumerate() {
428        if !kept.contains(&k) {
429            f.assign_item = None;
430        }
431    }
432}
433
434/// How severe IBM's message for a card's option is: an invalid suboption is an error and the option
435/// is discarded, and a removed option a warning or informational message (assumptions
436/// [`numeric::assumptions::INVALID_OPTION_DISCARDED`], [`numeric::assumptions::NUMPROC_MIG_WARNS`]
437/// and [`numeric::assumptions::OPTIONS_WITHOUT_EFFECT`]). A code page ironwork does not carry
438/// stops the compile, as ironwork cannot read the program in it.
439fn option_severity(e: &numeric::options::OptionError) -> Severity {
440    use numeric::options::OptionError;
441    match e {
442        OptionError::BadSuboption { .. } => Severity::Error,
443        OptionError::Removed { .. } | OptionError::NoEffect { warning: true, .. } => Severity::Warning,
444        OptionError::NoEffect { warning: false, .. } => Severity::Informational,
445        OptionError::UnsupportedCodePage(_) | OptionError::UnknownFlag(_) => Severity::Severe,
446    }
447}
448
449/// CURRENCY(literal) makes its character the currency symbol, standing for itself, of a program
450/// with no CURRENCY SIGN clause, in place of $; a program with one ignores the option
451/// (Programming Guide SC27-8714-03, p. 358). A hexadecimal literal whose character the option may
452/// not name is discarded with an error, as an invalid suboption is (assumption
453/// [`numeric::assumptions::CURRENCY_OPTION`]).
454fn default_currency(program: &mut Program, options: &mut Options, errors: &mut Vec<Error>) {
455    match options.currency_symbol() {
456        Some(Ok(symbol)) if program.environment.currency.is_empty() => program.environment.currency.push(CurrencySign { value: symbol.to_string(), symbol, hex: None }),
457        Some(Err(c)) => {
458            errors.push(syntax::messages::IWO0002.at(Pos::default(), format!("CBL CURRENCY: code page {} reads its byte as {c:?}, which cannot be a currency symbol", options.codepage)).graded(Severity::Error));
459            options.currency = None;
460        }
461        _ => {}
462    }
463}
464
465/// NSYMBOL(DBCS) makes a PICTURE of N, with or without B, and no USAGE, its own or a group's,
466/// USAGE DISPLAY-1 (Programming Guide SC27-8714-03, p. 388). NSYMBOL(NATIONAL) with NODBCS on the
467/// cards is IBM's conflict: DBCS in effect, with a warning (p. 344; assumption
468/// [`numeric::assumptions::NSYMBOL_DBCS`]).
469fn national_symbols(program: &mut Program, options: &mut Options, errors: &mut Vec<Error>) {
470    if options.nsymbol == numeric::Nsymbol::National {
471        if !options.dbcs && program.options.iter().any(|o| numeric::options::switch(o, "NSYMBOL").is_some()) {
472            errors.push(syntax::messages::IWO0003.at(Pos::default(), "CBL NODBCS: NSYMBOL(NATIONAL) requires DBCS, which is in effect"));
473            options.dbcs = true;
474        }
475        return;
476    }
477    let only_n = |p: &str| p.chars().any(|c| c.eq_ignore_ascii_case(&'N')) && p.chars().all(|c| c.eq_ignore_ascii_case(&'N') || c.eq_ignore_ascii_case(&'B') || c.is_ascii_digit() || matches!(c, '(' | ')'));
478    let lists = [&mut program.working_storage, &mut program.local_storage, &mut program.linkage].into_iter().chain(program.files.iter_mut().map(|f| &mut f.records));
479    for entries in lists {
480        let mut groups: Vec<(u8, bool)> = Vec::new();
481        for e in entries.iter_mut().filter(|e| !matches!(e.level, 66 | 88)) {
482            let level = if e.level == 77 { 1 } else { e.level };
483            while groups.last().is_some_and(|&(l, _)| l >= level) {
484                groups.pop();
485            }
486            let usage = e.usage.is_some() || groups.iter().any(|&(_, u)| u);
487            groups.push((level, e.usage.is_some()));
488            if !usage && e.picture.as_deref().is_some_and(only_n) {
489                e.usage = Some(Usage::Dbcs);
490            }
491        }
492    }
493}
494
495/// IBM's warning for a program with no STOP RUN, GOBACK or EXIT PROGRAM, which may run past its
496/// end; one that leaves by EXEC CICS RETURN or XCTL gets what `--cics-return-warning` says
497/// (assumption [`numeric::assumptions::NO_PROGRAM_END`]).
498fn program_end(program: &Program, options: &Options, errors: &mut Vec<Error>) {
499    use numeric::CicsReturnWarning;
500    let mut all = Vec::new();
501    program.paragraphs.iter().for_each(|p| inner_statements(&p.statements, &mut all));
502    if all.iter().any(|s| matches!(s, Stmt::StopRun { .. } | Stmt::Goback { .. } | Stmt::ExitProgram { .. })) {
503        return;
504    }
505    let cics_end = all.iter().find_map(|s| match s {
506        Stmt::Exec(b) if b.kind == ExecKind::Cics && matches!(b.command.as_str(), "RETURN" | "XCTL") => Some(b.command.as_str()),
507        _ => None,
508    });
509    match (cics_end, options.cics_return_warning) {
510        (Some(_), CicsReturnWarning::Never) => {}
511        (Some(command), CicsReturnWarning::Once) => errors.push(syntax::messages::IWP0002.at(Pos::default(), format!(
512            "IGYPS2091-W not given: the program ends with EXEC CICS {command}, which the CICS translator turns into a CALL; --cics-return-warning=always gives the warning, =never drops this note"
513        )).graded(Severity::Informational)),
514        (None, _) | (Some(_), CicsReturnWarning::Always) => {
515            errors.push(syntax::messages::IWC0055.at(Pos::default(), "no STOP RUN, GOBACK or EXIT PROGRAM in the program: check that it ends"));
516        }
517    }
518}
519
520/// Under INTDATE(LILIAN) a CALL of the literal 'CEECBLDY', whose ANSI integer date neither the date
521/// functions nor the callable services could then read, is diagnosed and calls CEEDAYS
522/// (Programming Guide SC27-8714-03, p. 375; assumption
523/// [`numeric::assumptions::CEECBLDY_UNDER_LILIAN`]).
524fn ceecbldy_to_ceedays(stmts: &mut [Stmt], errors: &mut Vec<Error>) {
525    for s in stmts {
526        if let Stmt::Call(c) = s
527            && let Operand::Literal(Literal::Alnum(name)) = &mut c.target
528            && name.trim().eq_ignore_ascii_case("CEECBLDY")
529        {
530            *name = "CEEDAYS".into();
531            errors.push(syntax::messages::IWC0056.at(c.pos, "CALL 'CEECBLDY' under INTDATE(LILIAN): CEECBLDY gives an ANSI integer date, which nothing can use under LILIAN, so the CALL is to CEEDAYS"));
532        }
533        for body in oo::bodies_mut(s) {
534            ceecbldy_to_ceedays(body, errors);
535        }
536    }
537}
538
539/// Whether messages keep a program from running: the COMPILE option in force stops its object code,
540/// or, as IGYWCLG bypasses its GO step, the return code is above 8. See
541/// [`numeric::assumptions::REFUSED_FROM_S`] and [`numeric::assumptions::NOCOMPILE`].
542pub fn refused(messages: &[Error], options: &Options) -> bool {
543    let stops_at = options.object_code().stops_at().min(12);
544    stops_at == 0 || messages.iter().any(|m| m.severity.return_code() >= stops_at)
545}
546
547/// The rules of the ENTRY statement (Language Reference SC27-8713-03, pp. 339-340), and of ALTER
548/// and the GO TO it alters (pp. 318, 346-348).
549fn procedure_rules(program: &Program, layout: &Layout, entries: &[EntryPoint], options: &Options, errors: &mut Vec<Error>) {
550    let method = program.oo.as_ref().is_some_and(|o| matches!(o.unit, OoUnit::Method(_)));
551    for (k, e) in entries.iter().enumerate() {
552        if program.returning.is_some() {
553            errors.push(syntax::messages::IWC0057.at(e.pos, format!("ENTRY '{}': a program with PROCEDURE DIVISION RETURNING cannot have ENTRY statements", e.name)));
554        }
555        if e.name.eq_ignore_ascii_case(&program.id) || entries[..k].iter().any(|f| f.name == e.name) {
556            errors.push(syntax::messages::IWC0058.at(e.pos, format!("ENTRY '{}': the name is already the program's or another ENTRY's", e.name)));
557        }
558        for param in &e.using {
559            if !layout.linkage_roots.iter().enumerate().any(|(o, &i)| layout.is_argument(o) && layout.items[i].name.as_deref() == Some(param.name.as_str())) {
560                errors.push(syntax::messages::IWC0059.at(e.pos, format!("ENTRY '{}' USING {}: not an 01 or 77 item of the LINKAGE SECTION", e.name, param.name)));
561            }
562        }
563    }
564    let why_no_alter = if program.recursive {
565        Some("a RECURSIVE program")
566    } else if options.thread {
567        Some("a program compiled with THREAD")
568    } else {
569        method.then_some("a method")
570    };
571    for p in &program.paragraphs {
572        for s in &p.statements {
573            let mut inner = Vec::new();
574            oo::bodies(s).into_iter().for_each(|body| inner_statements(body, &mut inner));
575            for (t, nested) in std::iter::once((s, false)).chain(inner.into_iter().map(|t| (t, true))) {
576                match t {
577                    Stmt::Entry { name, pos, .. } if nested => {
578                        errors.push(syntax::messages::IWC0060.at(*pos, format!("ENTRY '{name}' must be a sentence of its own, not inside another statement")));
579                    }
580                    Stmt::GoTo { target: None, pos } => {
581                        if let Some(why) = why_no_alter {
582                            errors.push(syntax::messages::IWC0061.at(*pos, format!("a GO TO with no procedure-name cannot be used in {why}")));
583                        }
584                        if nested || !lone_go_to(p) {
585                            errors.push(syntax::messages::IWC0062.at(*pos, "a GO TO with no procedure-name must be its paragraph's only sentence"));
586                        }
587                    }
588                    Stmt::Alter { pairs, pos } => {
589                        if let Some(why) = why_no_alter {
590                            errors.push(syntax::messages::IWC0063.at(*pos, format!("ALTER cannot be used in {why}")));
591                        }
592                        for (from, to) in pairs {
593                            altered_paragraph(program, from, *pos, errors);
594                            if let Err(m) = procedure(program, to) {
595                                errors.push(m.at(*pos));
596                            }
597                        }
598                    }
599                    _ => {}
600                }
601            }
602        }
603    }
604}
605
606/// Enterprise COBOL's rules for the programs of a compilation (Language Reference, Conventions for
607/// program-names): under strict scope two of them may not share a name, and under
608/// `--unresolved-calls=fail` a static CALL must name one of them that it reaches, or a Language
609/// Environment service, as IBM's binder must resolve it.
610fn program_names(program: &Program, options: &Options, errors: &mut Vec<Error>) {
611    let strict = options.program_scope == numeric::ProgramScope::Strict;
612    if strict {
613        for (name, pos) in &program.duplicates {
614            let text = format!("{name}: two programs of {} have this name, and the programs of a separately compiled program each need their own (--program-scope=flexible allows it)", program.id);
615            errors.push(syntax::messages::IWC0295.at(*pos, text));
616        }
617    }
618    if options.unresolved_calls != numeric::UnresolvedCalls::Fail || options.dynam {
619        return;
620    }
621    let reached = |name: &str| {
622        let contained = program.callable.iter().chain(program.hidden.iter().filter(|_| !strict));
623        program.linked.iter().chain(contained).any(|n| n.eq_ignore_ascii_case(name)) || rt::le::provides(&name.to_ascii_uppercase())
624    };
625    let mut all = Vec::new();
626    program.paragraphs.iter().for_each(|p| inner_statements(&p.statements, &mut all));
627    for s in all {
628        if let Stmt::Call(c) = s
629            && let Operand::Literal(Literal::Alnum(name)) = &c.target
630            && !reached(name.trim_end())
631        {
632            let text = format!("CALL '{}': no program of the compilation has this name, and --unresolved-calls=fail refuses a static CALL the binder could not resolve", name.trim_end());
633            errors.push(syntax::messages::IWC0296.at(c.pos, text));
634        }
635    }
636}
637
638/// Every statement inside `stmts`, at any depth.
639fn inner_statements<'s>(stmts: &'s [Stmt], out: &mut Vec<&'s Stmt>) {
640    for s in stmts {
641        out.push(s);
642        oo::bodies(s).into_iter().for_each(|body| inner_statements(body, out));
643    }
644}
645
646/// A paragraph ALTER can name: one sentence, a GO TO without DEPENDING ON.
647fn altered_paragraph(program: &Program, name: &ProcName, pos: Pos, errors: &mut Vec<Error>) {
648    match procedure(program, name) {
649        Err(m) => errors.push(m.at(pos)),
650        Ok((i, _)) if program.paragraphs[i].is_section => errors.push(syntax::messages::IWC0064.at(pos, format!("ALTER {}: a section, where ALTER names a paragraph", name.name))),
651        Ok((i, _)) if !lone_go_to(&program.paragraphs[i]) => {
652            errors.push(syntax::messages::IWC0065.at(pos, format!("ALTER {}: the paragraph must hold one sentence, a GO TO without DEPENDING ON", name.name)));
653        }
654        Ok(_) => {}
655    }
656}
657
658fn lone_go_to(p: &Paragraph) -> bool {
659    matches!(p.statements.as_slice(), [Stmt::GoTo { .. }] | [Stmt::GoTo { .. }, Stmt::SentenceEnd])
660}
661
662/// The last paragraph of the section that paragraph `i` is in, or `i` when there are no sections.
663/// END DECLARATIVES ends a section as a section header does.
664pub fn section_end(program: &Program, i: usize) -> usize {
665    let paragraphs = &program.paragraphs;
666    let declaratives = program.report_writer.procedure_start;
667    let (floor, ceiling) = if i < declaratives { (0, declaratives) } else { (declaratives, paragraphs.len()) };
668    let Some(header) = (floor..=i).rev().find(|&j| paragraphs[j].is_section) else { return i };
669    (header + 1..ceiling).take_while(|&j| !paragraphs[j].is_section).last().unwrap_or(header)
670}
671
672/// Qualifies each unqualified paragraph-name that more than one section holds with the section it
673/// is written in, when that section holds it: within its own section a paragraph-name needs no
674/// qualifier, so every later lookup must find that paragraph (assumption C151).
675fn qualify_in_own_section(program: &mut Program) {
676    for i in 0..program.paragraphs.len() {
677        let Some(section) = program.paragraphs[i].section.clone() else { continue };
678        let mut statements = std::mem::take(&mut program.paragraphs[i].statements);
679        oo::each_mut(&mut statements, &mut |s| {
680            for name in procedure_names_mut(s) {
681                if name.section.is_none() && names_own_paragraph(&program.paragraphs, &name.name, &section) {
682                    name.section = Some(section.clone());
683                }
684            }
685        });
686        program.paragraphs[i].statements = statements;
687    }
688}
689
690/// Whether an unqualified `name` written in `section` names that section's own paragraph: more than
691/// one procedure has the name, and `section` holds a paragraph of it.
692fn names_own_paragraph(paragraphs: &[Paragraph], name: &str, section: &str) -> bool {
693    paragraphs.iter().filter(|p| p.name == name).count() > 1 && paragraphs.iter().any(|p| p.name == name && !p.is_section && p.section.as_deref() == Some(section))
694}
695
696/// `procedure` for a name written in paragraph `from` that no statement holds as a procedure-name,
697/// such as a WHENEVER or HANDLE label or a USE FOR DEBUGGING operand, qualified with `from`'s
698/// section as `qualify_in_own_section` qualifies a statement's.
699pub fn procedure_from(program: &Program, p: &ProcName, from: usize) -> Result<(usize, usize), Unnamed> {
700    match program.paragraphs.get(from).and_then(|q| q.section.as_deref()) {
701        Some(section) if p.section.is_none() && names_own_paragraph(&program.paragraphs, &p.name, section) => {
702            procedure(program, &ProcName { name: p.name.clone(), section: Some(section.to_owned()) })
703        }
704        _ => procedure(program, p),
705    }
706}
707
708/// An EVALUATE subject that is a condition-name is a condition, and so are its WHEN objects
709/// written as names, which the parser could not tell from values.
710/// A DBCS item's VALUE, and a condition-name's of a DBCS item, is a DBCS literal no longer than the
711/// item, SPACE, or ALL with a DBCS literal; a DBCS literal is the VALUE of a DBCS item only
712/// (Language Reference SC27-8713-03, pp. 248-249).
713fn dbcs_values(layout: &Layout, errors: &mut Vec<Error>) {
714    let dbcs = |i: usize| matches!(layout.items[i].kind, rt::storage::Kind::Dbcs { .. });
715    let is_dbcs = |l: &Literal| matches!(l, Literal::Dbcs(_)) || matches!(l, Literal::All(inner) if matches!(**inner, Literal::Dbcs(_)));
716    let fits = |l: &Literal, i: usize| match l {
717        Literal::Dbcs(s) => s.chars().count() <= layout.items[i].size as usize / 2,
718        Literal::Figurative(syntax::ast::Figurative::Space) => true,
719        l => is_dbcs(l),
720    };
721    let values = layout.items.iter().enumerate().filter_map(|(i, it)| it.value.as_ref().map(|v| (i, v, it.pos)));
722    let conditions = layout.conditions.iter().flat_map(|c| c.values.iter().flat_map(|(low, high)| std::iter::once(low).chain(high)).chain(&c.false_value).map(move |v| (c.item, v, layout.items[c.item].pos)));
723    for (i, value, pos) in values.chain(conditions) {
724        let name = layout.items[i].name.as_deref().unwrap_or("FILLER");
725        if dbcs(i) && !fits(value, i) {
726            errors.push(syntax::messages::IWC0066.at(pos, format!("{name}: a DBCS item's VALUE is a DBCS literal of at most {} characters, SPACE or ALL with a DBCS literal", layout.items[i].size / 2)));
727        } else if !dbcs(i) && is_dbcs(value) {
728            errors.push(syntax::messages::IWC0067.at(pos, format!("{name}: a DBCS literal can be the VALUE of a DBCS item only")));
729        }
730    }
731}
732
733/// The environment-names DISPLAY UPON takes, directly or by a mnemonic-name (Language Reference
734/// SC27-8713-03, pp. 126, 334).
735const DISPLAY_DEVICES: &[&str] = &["SYSOUT", "SYSLIST", "SYSLST", "SYSPUNCH", "SYSPCH", "CONSOLE"];
736
737/// A numeric item's VALUE literal must be numeric (Language Reference SC27-8713-03, p. 246); a
738/// figurative constant stands for a literal as its own rules allow (p. 17).
739fn numeric_values(layout: &Layout, errors: &mut Vec<Error>) {
740    for item in layout.items.iter().filter(|i| i.children.is_empty() && i.kind.is_numeric()) {
741        let what = match item.value {
742            Some(Literal::Alnum(_) | Literal::Hex(_)) => "an alphanumeric literal",
743            Some(Literal::National(_)) => "a national literal",
744            _ => continue,
745        };
746        let name = item.name.as_deref().unwrap_or("FILLER");
747        errors.push(syntax::messages::IWC0068.at(item.pos, format!("VALUE of {name}: {what}, where a numeric item's VALUE literal must be numeric")));
748    }
749}
750
751/// A numeric-edited item's VALUE is an alphanumeric literal, a national one under USAGE NATIONAL,
752/// or a figurative constant, in edited form (Language Reference SC27-8713-03, pp. 246-247;
753/// IGYGR1080-S). Micro Focus and GnuCOBOL also take a numeric literal, edited as a MOVE edits it.
754fn edited_values(layout: &Layout, compliance: numeric::Compliance, errors: &mut Vec<Error>) {
755    for item in layout.items.iter().filter(|i| matches!(i.kind, rt::storage::Kind::NumericEdited { .. }) && matches!(i.value, Some(Literal::Number(_)))) {
756        let name = item.name.as_deref().unwrap_or("FILLER");
757        errors.push(if compliance == numeric::Compliance::Extended {
758            syntax::messages::IWX0012.at(item.pos, format!("a numeric literal as a numeric-edited item's VALUE (Micro Focus and GnuCOBOL; Enterprise COBOL takes an alphanumeric literal in edited form): {name} starts as the literal moved to it"))
759        } else {
760            syntax::messages::IWC0292.at(item.pos, format!("VALUE of {name}: a numeric literal, where a numeric-edited item's VALUE is an alphanumeric literal or a figurative constant written in edited form; --compliance extended edits the number into it"))
761        });
762    }
763}
764
765fn condition_subjects(program: &mut Program, layout: &Layout) {
766    let is_condition = |r: &Ref| matches!(layout.resolve(&r.name, &r.qualifiers, r.pos), Ok(layout::Resolved::Condition(_)));
767    for p in &mut program.paragraphs {
768        oo::each_mut(&mut p.statements, &mut |s| {
769            let Stmt::Evaluate { subjects, whens, .. } = s else { return };
770            for (k, subject) in subjects.iter_mut().enumerate() {
771                let Subject::Expr(Expr::Operand(Operand::Ref(r))) = subject else { continue };
772                if !is_condition(r) {
773                    continue;
774                }
775                *subject = Subject::Cond(Cond::Name(r.clone()));
776                for alternative in whens.iter_mut().flat_map(|w| w.alternatives.iter_mut()) {
777                    let cond = match alternative.get(k) {
778                        Some(Object::Value { not, from: Expr::Operand(Operand::Ref(o)), thru: None }) => {
779                            let c = Cond::Name(o.clone());
780                            if *not { Cond::Not(Box::new(c)) } else { c }
781                        }
782                        _ => continue,
783                    };
784                    alternative[k] = Object::Cond(cond);
785                }
786            }
787        });
788    }
789}
790
791/// The procedure-names statement `s` itself names, not those of the statements it holds.
792fn procedure_names_mut(s: &mut Stmt) -> Vec<&mut ProcName> {
793    match s {
794        Stmt::PerformProc { from, thru, .. } => std::iter::once(from).chain(thru.as_mut()).collect(),
795        Stmt::GoTo { target, .. } => target.iter_mut().collect(),
796        Stmt::GoToDepending { targets, .. } => targets.iter_mut().collect(),
797        Stmt::Alter { pairs, .. } => pairs.iter_mut().flat_map(|(from, to)| [from, to]).collect(),
798        Stmt::XmlParse(x) => std::iter::once(&mut x.procedure).chain(x.thru.as_mut()).collect(),
799        Stmt::Sorting(so) => match &mut **so {
800            Sorting::Sort(st) => [st.input.as_mut(), st.output.as_mut()]
801                .into_iter()
802                .flatten()
803                .flat_map(|io| match io {
804                    SortIo::Procedure { from, thru } => std::iter::once(from).chain(thru.as_mut()).collect(),
805                    SortIo::Files(_) => Vec::new(),
806                })
807                .collect(),
808            _ => Vec::new(),
809        },
810        _ => Vec::new(),
811    }
812}
813
814/// Why a procedure-name names no one procedure: the catalogue's message and its text.
815#[derive(Clone, Debug, PartialEq, Eq)]
816pub struct Unnamed {
817    pub message: Message,
818    pub text: String,
819}
820
821impl Unnamed {
822    pub fn at(self, pos: Pos) -> Error {
823        self.message.at(pos, self.text)
824    }
825}
826
827impl std::fmt::Display for Unnamed {
828    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829        f.write_str(&self.text)
830    }
831}
832
833impl From<Unnamed> for String {
834    fn from(u: Unnamed) -> Self {
835        u.text
836    }
837}
838
839/// The first and last paragraph a procedure name covers: one paragraph, or a whole section.
840pub fn procedure(program: &Program, p: &ProcName) -> Result<(usize, usize), Unnamed> {
841    let found: Vec<usize> = program
842        .paragraphs
843        .iter()
844        .enumerate()
845        .filter(|(_, q)| q.name == p.name && (p.section.is_none() || (q.section == p.section && !q.is_section)))
846        .map(|(i, _)| i)
847        .collect();
848    match found.as_slice() {
849        [i] if program.paragraphs[*i].is_section => Ok((*i, section_end(program, *i))),
850        [i] => Ok((*i, *i)),
851        [] => Err(Unnamed { message: IWC0003, text: format!("no paragraph or section named {}", p.name) }),
852        _ => Err(Unnamed { message: IWC0004, text: format!("{} names more than one paragraph; qualify it with OF and its section", p.name) }),
853    }
854}
855
856/// An 01 or 77 entry with OCCURS, which Micro Focus and GnuCOBOL take and Enterprise COBOL does not,
857/// read under `--compliance extended` as an unnamed 01 group holding the table one level down, its
858/// subordinate entries a level lower too, with IWX0019-W. An entry REDEFINES, EXTERNAL or GLOBAL
859/// names, or one with a level-49 entry under it, is left for the layout to refuse (IWC0027).
860/// Assumption C461.
861/// Returns where each record was inserted, counted in the entries as they were before.
862fn top_level_tables(entries: &mut Vec<DataEntry>, errors: &mut Vec<Error>) -> Vec<usize> {
863    let mut inserted = Vec::new();
864    let mut at = 0;
865    while at < entries.len() {
866        let e = &entries[at];
867        let end = at + 1 + entries[at + 1..].iter().take_while(|d| (2..=49).contains(&d.level) || d.level == 88).count();
868        let movable = matches!(e.level, 1 | 77) && e.occurs.is_some() && e.redefines.is_none() && !e.external && !e.global && entries[at + 1..end].iter().all(|d| d.level != 49);
869        if !movable {
870            at = end.max(at + 1);
871            continue;
872        }
873        let name = e.name.clone().unwrap_or_else(|| "FILLER".into());
874        errors.push(syntax::messages::IWX0019.at(e.pos, format!("OCCURS at level {:02} (Micro Focus and GnuCOBOL; Enterprise COBOL takes OCCURS only at levels 02 to 49): {name} is read as a table in a record of its own", e.level)));
875        let record = DataEntry { level: 1, name: None, spelled: None, picture: None, usage: None, value: None, redefines: None, occurs: None, occurs_min: None, depending_on: None, sign: None, justified: false, sync: false, blank_when_zero: false, indexed_by: Vec::new(), keys: Vec::new(), condition_values: Vec::new(), false_value: None, renames: None, object_class: None, external: false, global: false, any_length: false, based: false, pos: e.pos };
876        for d in &mut entries[at..end] {
877            if d.level != 88 {
878                d.level = if d.level == 77 { 2 } else { d.level + 1 };
879            }
880        }
881        entries.insert(at, record);
882        inserted.push(at - inserted.len());
883        at = end + 1;
884    }
885    inserted
886}
887
888/// Level-66 entries followed by an entry of levels 02 to 49 of the same record, which cobc 3.2
889/// takes under its IBM and Micro Focus dialects, moved under `--compliance extended` to follow the
890/// record's last entry, with IWX0032-W. A RENAMES names what it renames, so where it stands changes
891/// no storage. One followed by a level-88 entry is left for the layout to refuse (IWC0028).
892/// Assumption C471.
893fn renames_inside_records(entries: &mut [DataEntry], errors: &mut Vec<Error>) {
894    let mut at = 0;
895    while at < entries.len() {
896        if entries[at].level != 66 {
897            at += 1;
898            continue;
899        }
900        let run = entries[at..].iter().take_while(|e| e.level == 66).count();
901        let next = at + run;
902        if !entries.get(next).is_some_and(|e| (2..=49).contains(&e.level)) {
903            at = next;
904            continue;
905        }
906        let record = entries[..at].iter().rev().find(|e| e.level == 1).and_then(|e| e.name.clone()).unwrap_or_else(|| "FILLER".into());
907        let end = next + entries[next..].iter().take_while(|e| !matches!(e.level, 1 | 66 | 77)).count();
908        for e in &entries[at..next] {
909            let name = e.name.as_deref().unwrap_or("FILLER");
910            errors.push(syntax::messages::IWX0032.at(e.pos, format!("a level-66 entry before the end of its record (GnuCOBOL's IBM and Micro Focus dialects; Enterprise COBOL writes a record's RENAMES entries after its last entry): {name} is read as following {record}'s last entry")));
911        }
912        entries[at..end].rotate_left(run);
913        at = end - run;
914    }
915}
916
917/// A paragraph or section named FOREVER where PERFORM FOREVER is read as the endless loop
918/// (`--compliance extended`): Enterprise COBOL would perform it.
919fn forever_names(program: &Program, errors: &mut Vec<Error>) {
920    let mut endless = false;
921    for p in &program.paragraphs {
922        oo::each(&p.statements, &mut |s| endless |= matches!(s, Stmt::PerformInline { repeat: Loop::Forever, .. } | Stmt::PerformProc { repeat: Loop::Forever, .. }));
923    }
924    if !endless {
925        return;
926    }
927    for p in program.paragraphs.iter().filter(|p| p.name == "FOREVER") {
928        let what = if p.is_section { "SECTION" } else { "paragraph" };
929        errors.push(syntax::messages::IWC0304.at(p.pos, format!("{what} FOREVER: under --compliance extended PERFORM FOREVER is Micro Focus's and GnuCOBOL's endless loop, not a PERFORM of it; compile the program under strict")));
930    }
931}
932
933/// BASED records of WORKING-STORAGE and LOCAL-STORAGE, GnuCOBOL's and Micro Focus's items with no
934/// storage until SET ADDRESS OF gives them some, moved with what is subordinate to them to the
935/// LINKAGE SECTION, where Enterprise COBOL describes such items, with IWX0046-W (assumption C486).
936/// Gives `declared`, the count of WORKING-STORAGE entries the source declares, less those moved.
937fn based_items(program: &mut Program, declared: usize, errors: &mut Vec<Error>) -> usize {
938    let mut moved = Vec::new();
939    let mut declared_left = declared;
940    for (entries, storage) in [(&mut program.working_storage, true), (&mut program.local_storage, false)] {
941        let mut kept = Vec::with_capacity(entries.len());
942        let mut taking = false;
943        for (k, e) in std::mem::take(entries).into_iter().enumerate() {
944            if e.based && !matches!(e.level, 1 | 77) {
945                errors.push(syntax::messages::IWR0077.at(e.pos, format!("BASED on {}: ironwork reads BASED on an 01 or 77 entry", e.name.as_deref().unwrap_or("FILLER"))));
946            }
947            if matches!(e.level, 1 | 77) {
948                taking = e.based;
949                if taking {
950                    let name = e.name.as_deref().unwrap_or("FILLER");
951                    errors.push(syntax::messages::IWX0046.at(e.pos, format!("BASED (GnuCOBOL and Micro Focus; Enterprise COBOL describes such an item in the LINKAGE SECTION): {name} has no storage until SET ADDRESS OF gives it some")));
952                }
953            }
954            if taking {
955                declared_left -= usize::from(storage && k < declared);
956                moved.push(e);
957            } else {
958                kept.push(e);
959            }
960        }
961        *entries = kept;
962    }
963    program.linkage.extend(moved);
964    declared_left
965}
966
967/// BINARY-CHAR, Micro Focus's and GnuCOBOL's one-byte binary: a warning naming its range under
968/// `--compliance extended` (assumption C460), refused under strict.
969fn binary_chars(program: &Program, options: &Options, errors: &mut Vec<Error>) {
970    let entries = program.working_storage.iter().chain(&program.local_storage).chain(&program.linkage).chain(program.files.iter().flat_map(|f| &f.records));
971    for e in entries {
972        let Some(Usage::BinaryChar { signed }) = e.usage else { continue };
973        if options.compliance == numeric::Compliance::Strict {
974            errors.push(syntax::messages::IWC0293.at(e.pos, "BINARY-CHAR: Micro Focus's and GnuCOBOL's one-byte binary, not Enterprise COBOL's; --compliance extended reads it"));
975            continue;
976        }
977        let name = e.name.as_deref().unwrap_or("FILLER");
978        let range = if signed { "-128 to 127" } else { "0 to 255" };
979        errors.push(syntax::messages::IWX0016.at(e.pos, format!("BINARY-CHAR (Micro Focus and GnuCOBOL; Enterprise COBOL's binary items are two, four or eight bytes): {name} is one byte of binary, {range}")));
980    }
981}
982
983/// COMP-X, and COMP-5 with an alphanumeric PICTURE, Micro Focus's binary of one to eight bytes: a
984/// warning naming its bytes and range under `--compliance extended` (assumptions C467 and C468),
985/// refused under strict. Layout refuses a PIC X(n) of more than eight bytes.
986fn micro_focus_binaries(program: &Program, options: &Options, errors: &mut Vec<Error>) {
987    let entries = program.working_storage.iter().chain(&program.local_storage).chain(&program.linkage).chain(program.files.iter().flat_map(|f| &f.records));
988    for e in entries {
989        let Some(usage @ (Usage::CompX | Usage::NativeBinary)) = e.usage else { continue };
990        let Some(pic) = e.picture.as_deref().and_then(|p| picture::analyse(p).ok()) else { continue };
991        let (native, digits, signed) = match (usage, pic.category) {
992            (Usage::CompX, picture::Category::Numeric) if pic.digits <= 18 => (numeric::Native::CompX, pic.digits as u8, pic.signed),
993            (Usage::CompX, picture::Category::Alphanumeric) if pic.size <= 8 => (numeric::Native::CompX, numeric::binary::alphanumeric_digits(pic.size), false),
994            (Usage::NativeBinary, picture::Category::Alphanumeric) if pic.size <= 8 => (numeric::Native::Comp5Bytes, numeric::binary::alphanumeric_digits(pic.size), false),
995            _ => continue,
996        };
997        if options.compliance == numeric::Compliance::Strict {
998            errors.push(if native == numeric::Native::CompX {
999                syntax::messages::IWC0301.at(e.pos, "COMP-X: Micro Focus's binary in the fewest bytes its digits need, not Enterprise COBOL's; --compliance extended reads it")
1000            } else {
1001                syntax::messages::IWC0302.at(e.pos, "PIC X(n) COMP-5: Micro Focus's and GnuCOBOL's binary of n bytes, where Enterprise COBOL's COMP-5 takes a numeric PICTURE; --compliance extended reads it")
1002            });
1003            continue;
1004        }
1005        let name = e.name.as_deref().unwrap_or("FILLER");
1006        let bytes = numeric::binary::Binary { digits, signed, native }.bytes();
1007        let bits = 8 * bytes as u32;
1008        let range = if signed { format!("-{} to {}", 1u64 << (bits - 1), (1u64 << (bits - 1)) - 1) } else { format!("0 to {}", u64::MAX >> (64 - bits)) };
1009        let plural = if bytes == 1 { "" } else { "s" };
1010        errors.push(if native == numeric::Native::CompX {
1011            syntax::messages::IWX0025.at(e.pos, format!("COMP-X (Micro Focus; Enterprise COBOL's binary items are two, four or eight bytes): {name} is {bytes} byte{plural} of binary, {range}, shown in {digits} digits"))
1012        } else {
1013            syntax::messages::IWX0026.at(e.pos, format!("PIC X(n) COMP-5 (Micro Focus and GnuCOBOL; Enterprise COBOL's COMP-5 takes a numeric PICTURE): {name} is {bytes} byte{plural} of binary, {range}"))
1014        });
1015    }
1016}
1017
1018/// Under ARITH(COMPAT) a numeric or numeric-edited PICTURE, scaling positions P included, and a
1019/// fixed-point numeric literal hold at most 18 digits, and under ARITH(EXTEND) 31 (Language
1020/// Reference SC27-8713-03, pp. 45, 209, 217-218; Programming Guide SC27-8714-03, p. 349).
1021fn digit_limits(program: &Program, arith: numeric::options::Arith, errors: &mut Vec<Error>) {
1022    let max = arith.max_picture_digits();
1023    let option = match arith {
1024        numeric::options::Arith::Compat => "ARITH(COMPAT)",
1025        numeric::options::Arith::Extend => "ARITH(EXTEND)",
1026    };
1027    let entries = program.working_storage.iter().chain(&program.local_storage).chain(&program.linkage).chain(program.files.iter().flat_map(|f| &f.records));
1028    for e in entries {
1029        if let Some(p) = e.picture.as_deref()
1030            && let Ok(pic) = picture::analyse_with(p, crate::picture::Notation::of(&program.environment))
1031            && matches!(pic.category, picture::Category::Numeric | picture::Category::NumericEdited)
1032        {
1033            let positions = pic.digits + pic.scaling + pic.scale.saturating_sub(pic.digits);
1034            if positions > max {
1035                errors.push(syntax::messages::IWC0069.at(e.pos, format!("PICTURE {p}: {positions} digit positions, more than the {max} {option} allows")));
1036            }
1037        }
1038        let values = e.value.iter().chain(e.condition_values.iter().flat_map(|(low, high)| std::iter::once(low).chain(high))).chain(&e.false_value);
1039        for v in values {
1040            if let Literal::Number(t) = v
1041                && literal_digits(t) > max as usize
1042            {
1043                errors.push(syntax::messages::IWC0070.at(e.pos, format!("the literal {t} has more than the {max} digits {option} allows")));
1044            }
1045        }
1046    }
1047}
1048
1049fn literal_digits(t: &str) -> usize {
1050    t.chars().filter(char::is_ascii_digit).count()
1051}
1052
1053/// The usage of an INSPECT operand's characters.
1054#[derive(Clone, Copy, PartialEq, Eq)]
1055enum CharUsage {
1056    Display,
1057    National,
1058    Dbcs,
1059}
1060
1061impl CharUsage {
1062    fn of(kind: rt::storage::Kind) -> Self {
1063        match kind {
1064            rt::storage::Kind::National => Self::National,
1065            rt::storage::Kind::Dbcs { .. } => Self::Dbcs,
1066            _ => Self::Display,
1067        }
1068    }
1069}
1070
1071impl std::fmt::Display for CharUsage {
1072    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1073        f.write_str(match self {
1074            Self::Display => "DISPLAY",
1075            Self::National => "national",
1076            Self::Dbcs => "DBCS",
1077        })
1078    }
1079}
1080
1081/// How an INSPECT message names a literal, and its usage; None for a figurative constant, which
1082/// takes the inspected item's usage.
1083fn inspected_literal(l: &Literal) -> Option<(&'static str, CharUsage)> {
1084    match l {
1085        Literal::Figurative(_) => None,
1086        Literal::All(inner) => inspected_literal(inner),
1087        Literal::National(_) => Some(("a national literal", CharUsage::National)),
1088        Literal::Dbcs(_) => Some(("a DBCS literal", CharUsage::Dbcs)),
1089        Literal::Number(_) => Some(("a numeric literal", CharUsage::Display)),
1090        Literal::Alnum(_) | Literal::Hex(_) => Some(("an alphanumeric literal", CharUsage::Display)),
1091    }
1092}
1093
1094/// Resolves every name before the program runs, so a misspelling is a compile error.
1095/// The condition-names a JSON PARSE USING phrase names.
1096fn flag_conditions(flag: &Flag) -> Vec<&Ref> {
1097    match flag {
1098        Flag::Condition(c) => vec![c],
1099        Flag::Conditions(on, off) => vec![on, off],
1100        Flag::Literals(..) => Vec::new(),
1101    }
1102}
1103
1104struct Check<'a> {
1105    layout: &'a Layout,
1106    program: &'a Program,
1107    errors: &'a mut Vec<Error>,
1108    /// The statements are a debugging section's, which alone may reference DEBUG-ITEM.
1109    debugging: bool,
1110    /// The most digits a numeric literal has under the program's ARITH option.
1111    max_digits: u32,
1112    /// How many inline PERFORMs the statement is inside.
1113    inline_performs: usize,
1114    /// The user-defined functions the statements may invoke; None where none can be.
1115    functions: Option<&'a [function::Udf]>,
1116    /// Where each item of category alphabetic is declared.
1117    alphabetic: &'a [Pos],
1118    /// The statement whose conditions are being checked, which their messages name.
1119    at: Pos,
1120    /// The paragraph the statements are in, from which a HANDLE label is found.
1121    paragraph: usize,
1122    /// `--compliance extended` is in force.
1123    extended: bool,
1124    /// Where an ACCEPT ... FROM ENVIRONMENT's DISPLAY UPON ENVIRONMENT-NAME is, whose ACCEPT the
1125    /// message is given at.
1126    environment_named: Option<Pos>,
1127}
1128
1129impl Check<'_> {
1130    fn statements(&mut self, stmts: &[Stmt]) {
1131        for s in stmts {
1132            self.statement(s);
1133        }
1134    }
1135
1136    fn statement(&mut self, s: &Stmt) {
1137        linage::check_receivers(self.layout, s, self.errors);
1138        if let Stmt::If { pos, .. } | Stmt::Evaluate { pos, .. } | Stmt::PerformInline { pos, .. } | Stmt::PerformProc { pos, .. } = s {
1139            self.at = *pos;
1140        }
1141        if let Stmt::Search(se) = s {
1142            self.at = se.pos;
1143        }
1144        match s {
1145            Stmt::Move { from, to, .. } => {
1146                self.operand(from);
1147                if let Operand::Function(f) = from {
1148                    self.moved_function(f);
1149                }
1150                to.iter().for_each(|r| self.reference(r));
1151            }
1152            Stmt::Compute { targets, expr, size_error, .. } => {
1153                for t in targets {
1154                    self.reference(&t.r);
1155                    self.arithmetic_receiver("COMPUTE", &t.r);
1156                }
1157                self.expr(expr);
1158                self.size_error(size_error.as_ref());
1159            }
1160            Stmt::Arith(a) => {
1161                let verb = match a.verb {
1162                    ArithVerb::Add => "ADD",
1163                    ArithVerb::Subtract => "SUBTRACT",
1164                    ArithVerb::Multiply => "MULTIPLY",
1165                    ArithVerb::Divide => "DIVIDE",
1166                };
1167                for (t, e) in &a.computations {
1168                    self.reference(&t.r);
1169                    self.arithmetic_receiver(verb, &t.r);
1170                    self.expr(e);
1171                }
1172                if let Some((t, x, y)) = &a.remainder {
1173                    self.reference(&t.r);
1174                    self.arithmetic_receiver(verb, &t.r);
1175                    self.expr(x);
1176                    self.expr(y);
1177                }
1178                self.size_error(a.size_error.as_ref());
1179            }
1180            Stmt::If { cond, then, otherwise, .. } => {
1181                self.cond(cond);
1182                self.statements(then);
1183                self.statements(otherwise);
1184            }
1185            Stmt::PerformInline { body, repeat, .. } => {
1186                self.repeat(repeat);
1187                self.inline_performs += 1;
1188                self.statements(body);
1189                self.inline_performs -= 1;
1190            }
1191            Stmt::PerformProc { from, thru, repeat, pos } => {
1192                self.procedure(from, *pos);
1193                if let Some(t) = thru {
1194                    self.procedure(t, *pos);
1195                }
1196                self.repeat(repeat);
1197            }
1198            Stmt::Evaluate { subjects, whens, other, pos } => {
1199                for subject in subjects {
1200                    match subject {
1201                        Subject::Expr(e) => self.expr(e),
1202                        Subject::Cond(c) => self.cond(c),
1203                        Subject::Bool(_) => {}
1204                    }
1205                }
1206                for w in whens {
1207                    for alternative in &w.alternatives {
1208                        for (subject, object) in subjects.iter().zip(alternative) {
1209                            match object {
1210                                Object::Any => {}
1211                                Object::Bool(_) | Object::Cond(_) if matches!(subject, Subject::Expr(_)) => {
1212                                    self.errors.push(syntax::messages::IWC0071.at(*pos, "a condition as the WHEN object of a value subject"));
1213                                }
1214                                Object::Bool(_) => {}
1215                                Object::Cond(c) => self.cond(c),
1216                                Object::Value { .. } if !matches!(subject, Subject::Expr(_)) => {
1217                                    self.errors.push(syntax::messages::IWC0072.at(*pos, "a value as the WHEN object of a TRUE, FALSE or condition subject"));
1218                                }
1219                                Object::Value { from, thru, .. } => {
1220                                    self.expr(from);
1221                                    if let Some(t) = thru {
1222                                        self.expr(t);
1223                                    }
1224                                    if let Subject::Expr(e) = subject {
1225                                        std::iter::once(from).chain(thru).for_each(|o| self.comparison(e, o));
1226                                    }
1227                                }
1228                            }
1229                        }
1230                    }
1231                    self.statements(&w.body);
1232                }
1233                self.statements(other);
1234            }
1235            Stmt::Display { items, upon, screen, pos, .. } => {
1236                for o in items {
1237                    self.operand(o);
1238                    if let Operand::Function(f) = o {
1239                        self.displayed_function(f);
1240                    }
1241                }
1242                if let Some(screen) = screen.as_deref().filter(|s| s.screen.is_none()) {
1243                    self.screen("DISPLAY", screen);
1244                }
1245                if let Some(upon) = upon
1246                    && matches!(upon.device.as_str(), "ENVIRONMENT-NAME" | "ENVIRONMENT-VALUE")
1247                {
1248                    self.environment_display(upon, *pos);
1249                } else if let Some(upon) = upon
1250                    && upon.device == "ARGUMENT-NUMBER"
1251                {
1252                    self.argument_number(items, *pos);
1253                } else if let Some(upon) = upon
1254                    && upon.device == "SYSERR"
1255                    && self.extended
1256                {
1257                    self.errors.push(syntax::messages::IWX0056.at(*pos, "DISPLAY UPON SYSERR (GnuCOBOL and Micro Focus; Enterprise COBOL has no such device): the line is written to the run's standard error"));
1258                } else if let Some(upon) = upon
1259                    && !DISPLAY_DEVICES.contains(&upon.device.as_str())
1260                {
1261                    let why = if upon.name == upon.device {
1262                        "neither an environment-name DISPLAY writes to, SYSOUT, SYSLIST, SYSLST, SYSPUNCH, SYSPCH or CONSOLE, nor a mnemonic-name for one".to_owned()
1263                    } else {
1264                        format!("a mnemonic-name for {}, which DISPLAY does not write to", upon.device)
1265                    };
1266                    self.errors.push(syntax::messages::IWC0073.at(*pos, format!("DISPLAY UPON {}: {why}", upon.name)));
1267                }
1268            }
1269            Stmt::Open { files, pos } => files.iter().for_each(|(_, f)| self.file(f, *pos)),
1270            Stmt::DeleteFile { files, pos } => files.iter().for_each(|name| self.file(name, *pos)),
1271            Stmt::Close { files, pos } => {
1272                for (name, closing) in files {
1273                    self.file(name, *pos);
1274                    let keyed = self.program.files.iter().any(|f| f.name == *name && matches!(f.organization, Organization::Indexed | Organization::Relative));
1275                    if keyed && matches!(closing, Some(Closing::Volume | Closing::NoRewind)) {
1276                        self.errors.push(syntax::messages::IWC0074.at(*pos, format!("CLOSE {name}: REEL, UNIT and NO REWIND are not valid for an indexed or relative file")));
1277                    }
1278                }
1279            }
1280            Stmt::Read(r) => {
1281                self.file(&r.file, r.pos);
1282                if r.next {
1283                    self.not_random(&r.file, "READ NEXT", r.pos);
1284                }
1285                if let Some(into) = &r.into {
1286                    self.reference(into);
1287                }
1288                if let Some(key) = &r.key {
1289                    self.key_of(&r.file, key, false);
1290                }
1291                self.handlers(&r.at_end);
1292                self.handlers(&r.invalid);
1293            }
1294            Stmt::Write { record, from, invalid, pos, .. } | Stmt::Rewrite { record, from, invalid, pos } => {
1295                let verb = if matches!(s, Stmt::Write { .. }) { "WRITE" } else { "REWRITE" };
1296                self.reference(record);
1297                if let Ok(layout::Resolved::Item(i)) = self.layout.resolve(&record.name, &record.qualifiers, record.pos)
1298                    && self.layout.items[i].file.is_none()
1299                {
1300                    self.errors.push(syntax::messages::IWC0075.at(*pos, format!("{verb} {}: not a record of a file", record.name)));
1301                }
1302                if let Some(op) = from {
1303                    self.operand(op);
1304                }
1305                if let Stmt::Write { advancing, end_of_page, .. } = s {
1306                    if let Some(a) = advancing {
1307                        if let Advancing::Lines { count, .. } = a {
1308                            self.expr(count);
1309                        }
1310                        printer::check_write(self.program, self.layout, record, a, self.extended, *pos, self.errors);
1311                    }
1312                    linage::check_write(self.program, self.layout, record, advancing.as_ref(), end_of_page, *pos, self.errors);
1313                    self.handlers(end_of_page);
1314                }
1315                self.handlers(invalid);
1316            }
1317            Stmt::Delete { file, invalid, pos } => {
1318                self.keyed_file(file, "DELETE", *pos);
1319                self.handlers(invalid);
1320            }
1321            Stmt::Start { file, key, invalid, pos } => {
1322                self.keyed_file(file, "START", *pos);
1323                self.not_random(file, "START", *pos);
1324                if let Some((op, r)) = key {
1325                    if self.extended && matches!(op, RelOp::Lt | RelOp::Le) {
1326                        let written = if *op == RelOp::Lt { "<" } else { "NOT > or <=" };
1327                        self.errors.push(syntax::messages::IWX0036.at(*pos, format!("START KEY {written} (Micro Focus and GnuCOBOL; Enterprise COBOL's START takes =, >, NOT < or >=): the file is positioned at the last record whose key is {written} the value, which READ NEXT or READ PREVIOUS reads first")));
1328                    } else if !matches!(op, RelOp::Eq | RelOp::Gt | RelOp::Ge) {
1329                        self.errors.push(syntax::messages::IWC0076.at(*pos, "START KEY takes =, >, NOT < or >="));
1330                    }
1331                    self.key_of(file, r, true);
1332                }
1333                self.handlers(invalid);
1334            }
1335            Stmt::Initialize { targets, with, pos } => {
1336                for r in targets {
1337                    self.reference(r);
1338                    if self.item(r).is_some_and(|i| self.layout.items[i].level == 66) {
1339                        self.errors.push(syntax::messages::IWC0077.at(*pos, format!("INITIALIZE {}: a level-66 RENAMES item cannot be initialized", r.name)));
1340                    }
1341                    let items = &self.layout.items;
1342                    if self.item(r).is_some_and(|i| !items[i].moved_by.is_empty() || items[i].odo.iter().any(|&t| items[t].followed)) {
1343                        self.errors.push(syntax::messages::IWC0078.at(*pos, format!("INITIALIZE {}: a variably located item, or a group holding one, cannot be initialized (Language Reference p. 351)", r.name)));
1344                    }
1345                }
1346                if let Some(with) = with {
1347                    with.replacing.iter().for_each(|(_, by)| self.operand(by));
1348                    self.initialize_incompatible(targets, with, *pos);
1349                }
1350            }
1351            Stmt::GoTo { target: Some(target), pos } => self.procedure(target, *pos),
1352            Stmt::GoToDepending { targets, on, pos } => {
1353                targets.iter().for_each(|t| self.procedure(t, *pos));
1354                self.reference(on);
1355            }
1356            Stmt::GoTo { target: None, .. } | Stmt::Alter { .. } | Stmt::Entry { .. } => {}
1357            Stmt::Call(c) => {
1358                self.operand(&c.target);
1359                for arg in &c.using {
1360                    if let Some(op) = &arg.value {
1361                        self.operand(op);
1362                    }
1363                }
1364                if let Some(r) = &c.returning {
1365                    self.reference(r);
1366                }
1367                self.statements(c.on_exception.as_deref().unwrap_or_default());
1368                self.statements(c.not_on_exception.as_deref().unwrap_or_default());
1369            }
1370            Stmt::Cancel { targets, .. } => targets.iter().for_each(|t| self.operand(t)),
1371            Stmt::Set { set, .. } => match set {
1372                SetStmt::ConditionTrue(targets) => {
1373                    for r in targets {
1374                        self.reference_or_condition(r);
1375                        if let Ok(layout::Resolved::Condition(c)) = self.layout.resolve(&r.name, &r.qualifiers, r.pos)
1376                            && let Some(e) = switches::without_variable(self.layout, c, r)
1377                        {
1378                            self.errors.push(e);
1379                        }
1380                    }
1381                }
1382                SetStmt::ConditionFalse(targets) => {
1383                    for r in targets {
1384                        self.reference_or_condition(r);
1385                        if let Ok(layout::Resolved::Condition(c)) = self.layout.resolve(&r.name, &r.qualifiers, r.pos)
1386                            && self.layout.conditions[c].false_value.is_none()
1387                        {
1388                            self.errors.push(syntax::messages::IWC0079.at(r.pos, format!("SET {} TO FALSE: the condition-name has no WHEN SET TO FALSE value", r.name)));
1389                        }
1390                    }
1391                }
1392                SetStmt::To { targets, value } | SetStmt::Entry { targets, entry: value } => {
1393                    targets.iter().for_each(|r| self.reference(r));
1394                    self.operand(value);
1395                }
1396                SetStmt::AddressOf { targets, value } => {
1397                    targets.iter().for_each(|r| self.reference(r));
1398                    targets.iter().for_each(|r| scope::set_address(self.layout, r, self.errors));
1399                    self.operand(value);
1400                }
1401                SetStmt::UpDown { targets, by, .. } => {
1402                    targets.iter().for_each(|r| self.reference(r));
1403                    self.expr(by);
1404                }
1405                SetStmt::Switches(_) => {}
1406            },
1407            Stmt::Accept { target, from, exception, screen, pos } => {
1408                match screen {
1409                    Some(screen) if screen.screen.is_some() => {
1410                        for i in &screen.inputs {
1411                            self.reference(&i.field);
1412                            self.reference(&i.target);
1413                        }
1414                    }
1415                    Some(screen) => {
1416                        self.reference(target);
1417                        self.screen("ACCEPT", screen);
1418                    }
1419                    None => {
1420                        self.reference(target);
1421                        self.accept_source(*from, exception, target, *pos);
1422                    }
1423                }
1424                self.statements(exception.on.as_deref().unwrap_or_default());
1425                self.statements(exception.not_on.as_deref().unwrap_or_default());
1426            }
1427            Stmt::String(st) => {
1428                for (op, delimiter) in &st.sources {
1429                    self.operand(op);
1430                    if let Delimiter::By(d) = delimiter {
1431                        self.operand(d);
1432                    }
1433                }
1434                self.reference(&st.into);
1435                if let Some(p) = &st.pointer {
1436                    self.reference(p);
1437                }
1438                self.statements(st.on_overflow.as_deref().unwrap_or_default());
1439                self.statements(st.not_on_overflow.as_deref().unwrap_or_default());
1440            }
1441            Stmt::Unstring(u) => {
1442                self.reference(&u.source);
1443                u.delimiters.iter().for_each(|(_, d)| self.operand(d));
1444                for into in &u.into {
1445                    self.reference(&into.target);
1446                    into.delimiter_in.iter().chain(&into.count_in).for_each(|r| self.reference(r));
1447                }
1448                u.pointer.iter().chain(&u.tallying).for_each(|r| self.reference(r));
1449                self.statements(u.on_overflow.as_deref().unwrap_or_default());
1450                self.statements(u.not_on_overflow.as_deref().unwrap_or_default());
1451            }
1452            Stmt::Inspect(i) => {
1453                self.operand(&i.target);
1454                match &i.target {
1455                    Operand::Function(f) => self.inspected_function(f, i),
1456                    Operand::Ref(r) => self.inspected_usage(r, i),
1457                    _ => {}
1458                }
1459                for p in i.tallying.iter().chain(&i.replacing) {
1460                    p.pattern.iter().chain(&p.by).for_each(|o| self.operand(o));
1461                    if let Some(c) = &p.counter {
1462                        self.reference(c);
1463                    }
1464                    p.bounds.iter().for_each(|b| self.operand(&b.value));
1465                }
1466                if let Some((from, to, bounds)) = &i.converting {
1467                    self.operand(from);
1468                    self.operand(to);
1469                    bounds.iter().for_each(|b| self.operand(&b.value));
1470                }
1471            }
1472            Stmt::Search(se) => {
1473                self.reference_unsubscripted(&se.table);
1474                if let Some(v) = &se.varying {
1475                    self.reference(v);
1476                    self.search_varying(se, v);
1477                }
1478                self.statements(se.at_end.as_deref().unwrap_or_default());
1479                for (cond, body) in &se.whens {
1480                    self.cond(cond);
1481                    self.statements(body);
1482                }
1483            }
1484            Stmt::Exec(block) => self.exec_block(block),
1485            Stmt::Report(r) => report::check_statement(self.program, r, self.errors),
1486            Stmt::Invoke(i) => self.invoke(i),
1487            Stmt::XmlParse(x) => {
1488                self.reference(&x.document);
1489                if let Some(op) = &x.encoding {
1490                    self.operand(op);
1491                }
1492                self.procedure(&x.procedure, x.pos);
1493                if let Some(t) = &x.thru {
1494                    self.procedure(t, x.pos);
1495                }
1496                self.statements(x.on_exception.as_deref().unwrap_or_default());
1497                self.statements(x.not_on_exception.as_deref().unwrap_or_default());
1498            }
1499            Stmt::JsonParse(j) => {
1500                self.reference(&j.source);
1501                self.whole_table_reference(&j.into);
1502                let mut named: Vec<&Ref> = j.names.iter().map(|(r, _)| r).chain(&j.suppress).chain(j.ignoring.iter().flatten()).collect();
1503                for (item, flag, indicator) in &j.indicating {
1504                    named.push(item);
1505                    named.extend(indicator);
1506                    named.extend(flag_conditions(flag));
1507                }
1508                for (item, conversion) in &j.converting {
1509                    named.push(item);
1510                    if let ParseConversion::Boolean(flag) = conversion {
1511                        named.extend(flag_conditions(flag));
1512                    }
1513                }
1514                named.into_iter().for_each(|r| self.reference_unsubscripted(r));
1515                if let Some(Encoding::Ccsid(op)) = &j.encoding {
1516                    self.operand(op);
1517                }
1518                self.statements(j.on_exception.as_deref().unwrap_or_default());
1519                self.statements(j.not_on_exception.as_deref().unwrap_or_default());
1520            }
1521            Stmt::XmlGenerate(x) => {
1522                for r in [&x.receiver, &x.from].into_iter().chain(&x.count) {
1523                    self.reference(r);
1524                }
1525                let named = x.names.iter().map(|(r, _)| r).chain(x.types.iter().map(|(r, _)| r));
1526                let suppressed = x.suppress.iter().filter_map(|s| if let Suppression::Item { item, .. } = s { Some(item) } else { None });
1527                named.chain(suppressed).for_each(|r| self.reference_unsubscripted(r));
1528                for op in [&x.encoding, &x.namespace, &x.prefix].into_iter().flatten() {
1529                    self.operand(op);
1530                }
1531                self.statements(x.on_exception.as_deref().unwrap_or_default());
1532                self.statements(x.not_on_exception.as_deref().unwrap_or_default());
1533            }
1534            Stmt::JsonGenerate(g) => {
1535                for r in [&g.receiver].into_iter().chain(&g.count) {
1536                    self.reference(r);
1537                }
1538                self.whole_table_reference(&g.from);
1539                let mut named: Vec<&Ref> = g.names.iter().map(|(r, _)| r).collect();
1540                for s in &g.suppress {
1541                    if let Suppression::Item { item, .. } = s {
1542                        named.push(item);
1543                    }
1544                }
1545                for (item, conversion) in &g.converting {
1546                    named.push(item);
1547                    if let JsonConversion::Boolean(Marker::Condition(c)) = conversion {
1548                        named.push(c);
1549                    }
1550                }
1551                for i in &g.indicating {
1552                    named.push(&i.item);
1553                    named.extend(&i.indicator);
1554                    if let Marker::Condition(c) = &i.marker {
1555                        named.push(c);
1556                    }
1557                }
1558                named.into_iter().for_each(|r| self.reference_unsubscripted(r));
1559                if let Some(Encoding::Ccsid(op)) = &g.encoding {
1560                    self.operand(op);
1561                }
1562                self.statements(g.on_exception.as_deref().unwrap_or_default());
1563                self.statements(g.not_on_exception.as_deref().unwrap_or_default());
1564            }
1565            Stmt::Sorting(s) => self.sorting(s),
1566            // Language Reference SC27-8713-03, p. 344.
1567            Stmt::Exit { kind: kind @ (ExitKind::Perform | ExitKind::PerformCycle), pos } if self.inline_performs == 0 => {
1568                let exit = if *kind == ExitKind::Perform { "EXIT PERFORM" } else { "EXIT PERFORM CYCLE" };
1569                self.errors.push(syntax::messages::IWC0080.at(*pos, format!("{exit} must be inside an inline PERFORM")));
1570            }
1571            Stmt::Goback { .. } | Stmt::StopRun { .. } | Stmt::ExitProgram { .. } | Stmt::ExitMethod { .. } | Stmt::Continue { .. } | Stmt::Exit { .. } | Stmt::NextSentence | Stmt::SentenceEnd => {}
1572            Stmt::Corresponding(_) => unreachable!("CORRESPONDING is expanded before Check"),
1573        }
1574    }
1575
1576    fn size_error(&mut self, se: Option<&SizeError>) {
1577        if let Some(se) = se {
1578            self.statements(&se.on);
1579            self.statements(&se.not_on);
1580        }
1581    }
1582
1583    fn repeat(&mut self, repeat: &Loop) {
1584        match repeat {
1585            Loop::Once | Loop::Forever => {}
1586            Loop::Times(e) => self.expr(e),
1587            Loop::Until { cond, .. } => self.cond(cond),
1588            Loop::Varying { varying, after, .. } => {
1589                for v in std::iter::once(&**varying).chain(after) {
1590                    self.reference(&v.var);
1591                    self.expr(&v.from);
1592                    self.expr(&v.by);
1593                    self.cond(&v.until);
1594                    self.varying(v);
1595                }
1596            }
1597        }
1598    }
1599
1600    /// A VARYING or AFTER variable is a numeric elementary item or an index-name, and its FROM and
1601    /// BY an identifier, index-name or literal (Language Reference SC27-8713-03, p. 419).
1602    fn varying(&mut self, v: &Varying) {
1603        use rt::storage::Kind;
1604        if let Some(i) = self.item(&v.var)
1605            && !matches!(self.layout.items[i].kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_) | Kind::Index)
1606        {
1607            self.errors.push(syntax::messages::IWC0081.at(self.at, format!("PERFORM VARYING {}: not a numeric elementary item or an index-name", v.var.name)));
1608        }
1609        for (phrase, e) in [("FROM", &v.from), ("BY", &v.by)] {
1610            if !matches!(e, Expr::Operand(_)) {
1611                self.errors.push(syntax::messages::IWC0082.at(self.at, format!("PERFORM VARYING {} {phrase}: an arithmetic expression, where {phrase} takes an identifier, index-name or literal", v.var.name)));
1612            }
1613        }
1614    }
1615
1616    fn file(&mut self, name: &str, pos: Pos) {
1617        if !self.program.files.iter().any(|f| f.name == name) {
1618            self.errors.push(syntax::messages::IWC0083.at(pos, format!("no file named {name}")));
1619        }
1620    }
1621
1622    fn keyed_file(&mut self, name: &str, verb: &str, pos: Pos) {
1623        match self.program.files.iter().find(|f| f.name == name) {
1624            None => self.errors.push(syntax::messages::IWC0083.at(pos, format!("no file named {name}"))),
1625            Some(f) if !matches!(f.organization, Organization::Indexed | Organization::Relative) => {
1626                self.errors.push(syntax::messages::IWC0084.at(pos, format!("{verb} {name}: not an indexed or relative file")));
1627            }
1628            Some(_) => {}
1629        }
1630    }
1631
1632    fn not_random(&mut self, name: &str, verb: &str, pos: Pos) {
1633        if self.program.files.iter().any(|f| f.name == name && f.access == Access::Random) {
1634            self.errors.push(syntax::messages::IWC0085.at(pos, format!("{verb} {name}: the file's ACCESS MODE is RANDOM")));
1635        }
1636    }
1637
1638    fn handlers(&mut self, h: &Handlers) {
1639        self.statements(h.on.as_deref().unwrap_or_default());
1640        self.statements(h.not_on.as_deref().unwrap_or_default());
1641    }
1642
1643    /// A class-name condition tests a data item of USAGE DISPLAY (Language Reference SC27-8713-03,
1644    /// p. 269): alphabetic, alphanumeric, edited, zoned decimal or a group.
1645    fn class_name_subject(&mut self, e: &Expr, name: &str) {
1646        let Expr::Operand(Operand::Ref(r)) = e else {
1647            self.errors.push(syntax::messages::IWC0086.at(self.at, format!("class-name {name} tests a data item, not an expression")));
1648            return;
1649        };
1650        let Some(i) = self.item(r) else { return };
1651        if !matches!(self.layout.items[i].kind, layout::Kind::Group | layout::Kind::Alnum { .. } | layout::Kind::AlnumEdited { .. } | layout::Kind::NumericEdited { .. } | layout::Kind::Zoned { .. }) {
1652            self.errors.push(syntax::messages::IWC0087.at(r.pos, format!("class-name {name} tests a data item of USAGE DISPLAY, and {} is not one", r.name)));
1653        }
1654    }
1655
1656    fn item(&self, r: &Ref) -> Option<usize> {
1657        match self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
1658            Ok(layout::Resolved::Item(i)) => Some(i),
1659            _ => None,
1660        }
1661    }
1662
1663    /// The KEY of READ or START on an indexed file: a record key or alternate key of the file, or
1664    /// for START (`partial`) an item that starts where one does and is no longer.
1665    fn key_of(&mut self, file: &str, key: &Ref, partial: bool) {
1666        let Some(f) = self.program.files.iter().find(|f| f.name == file) else { return self.reference(key) };
1667        if key.qualifiers.is_empty() && f.split_key(&key.name).is_some() {
1668            return;
1669        }
1670        self.reference(key);
1671        if f.organization != Organization::Indexed {
1672            return;
1673        }
1674        let Some(item) = self.item(key).map(|i| &self.layout.items[i]) else { return };
1675        let fits = |r: &Ref| {
1676            self.item(r).map(|i| &self.layout.items[i]).is_some_and(|k| k.offset == item.offset && (k.size == item.size || partial && item.size < k.size))
1677        };
1678        let named = f.record_key.iter().chain(f.alternate_keys.iter().map(|(r, _)| r)).any(fits);
1679        if item.file.is_none() || !named {
1680            self.errors.push(syntax::messages::IWC0088.at(key.pos, format!("{}: not a key of {file}", key.name)));
1681        }
1682    }
1683
1684    /// An indexed file's keys lie in its records; a relative file's RELATIVE KEY lies outside them.
1685    fn file_keys(&mut self, k: usize) {
1686        let f = &self.program.files[k];
1687        let in_records = |c: &Self, r: &Ref| c.item(r).is_some_and(|i| c.layout.items[i].file == Some(k as u16));
1688        match f.organization {
1689            Organization::Indexed => {
1690                if f.record_key.is_none() {
1691                    self.errors.push(syntax::messages::IWC0089.at(f.pos, format!("{}: an indexed file needs a RECORD KEY", f.name)));
1692                }
1693                let keys = f.record_key.iter().chain(f.alternate_keys.iter().map(|(r, _)| r));
1694                let items: Vec<Ref> = keys.flat_map(|r| f.split_key(&r.name).map_or_else(|| vec![r.clone()], <[Ref]>::to_vec)).collect();
1695                for r in &items {
1696                    self.reference(r);
1697                    if self.item(r).is_some() && !in_records(self, r) {
1698                        self.errors.push(syntax::messages::IWC0090.at(r.pos, format!("{}: a key of {} must be in its records", r.name, f.name)));
1699                    }
1700                }
1701            }
1702            Organization::Relative => {
1703                if let Some(r) = &f.relative_key {
1704                    self.reference(r);
1705                    if in_records(self, r) {
1706                        self.errors.push(syntax::messages::IWC0091.at(r.pos, format!("{}: the RELATIVE KEY of {} must not be in its records", r.name, f.name)));
1707                    }
1708                } else if f.access != Access::Sequential {
1709                    self.errors.push(syntax::messages::IWC0092.at(f.pos, format!("{}: random or dynamic access needs a RELATIVE KEY", f.name)));
1710                }
1711            }
1712            _ => {}
1713        }
1714    }
1715
1716    /// RECORD IS VARYING's DEPENDING ON item is an elementary unsigned integer (Language Reference
1717    /// SC27-8713-03, p. 188).
1718    fn record_depending(&mut self, k: usize) {
1719        let f = &self.program.files[k];
1720        let Some(r) = &f.record_depending else { return };
1721        self.reference(r);
1722        if self.item(r).is_some() && linage::unsigned_integer(self.layout, r).is_none() {
1723            self.errors.push(syntax::messages::IWC0093.at(r.pos, format!("{}: the DEPENDING ON item of {} must be an elementary unsigned integer", r.name, f.name)));
1724        }
1725    }
1726
1727    /// LABEL RECORD IS data-name: Enterprise COBOL reads the clause as comments and still refuses a
1728    /// data-name it does not know, IGYGR1174-S.
1729    fn label_records(&mut self, k: usize) {
1730        let f = &self.program.files[k];
1731        for r in &f.label_records {
1732            if self.layout.resolve(&r.name, &r.qualifiers, r.pos).is_err() {
1733                self.errors.push(syntax::messages::IWC0324.at(r.pos, format!("LABEL RECORDS {}: not defined as a data-name", r.name)));
1734            }
1735        }
1736    }
1737
1738    /// RECORDING MODE F takes records of one length: not RECORD IS VARYING, not a two-bound RECORD
1739    /// clause, and level-01 records of one size with no OCCURS DEPENDING ON (Programming Guide
1740    /// SC27-8714-03, p. 227). Enterprise COBOL discards the file otherwise, IGYGR1211-S.
1741    fn recording_mode(&mut self, k: usize) {
1742        let f = &self.program.files[k];
1743        if f.recording != Some('F') {
1744            return;
1745        }
1746        let declared = (f.record_min, f.record_max);
1747        let lengths = match declared {
1748            (Some(min), Some(max)) if f.record_varying || min != max => Some((min, max)),
1749            _ => self.layout.record_lengths[k].filter(|(shortest, longest)| shortest != longest),
1750        };
1751        if let Some((shortest, longest)) = lengths {
1752            self.errors.push(syntax::messages::IWC0325.at(f.pos, format!("FD {}: RECORDING MODE F, but its records are {shortest} to {longest} bytes", f.name)));
1753        }
1754    }
1755
1756    /// A PASSWORD item is a WORKING-STORAGE item of category alphabetic, alphanumeric or
1757    /// alphanumeric-edited (Language Reference SC27-8713-03, p. 152).
1758    fn passwords(&mut self, k: usize) {
1759        let f = &self.program.files[k];
1760        for r in &f.passwords {
1761            self.reference(r);
1762            let Some(i) = self.item(r) else { continue };
1763            let item = &self.layout.items[i];
1764            let working = !item.local && item.file.is_none() && item.linkage.is_none();
1765            if !working || !matches!(item.kind, rt::storage::Kind::Alnum { .. } | rt::storage::Kind::AlnumEdited { .. } | rt::storage::Kind::Group) {
1766                self.errors.push(syntax::messages::IWC0094.at(r.pos, format!("{}: the PASSWORD of {} must be an alphabetic, alphanumeric or alphanumeric-edited item of WORKING-STORAGE", r.name, f.name)));
1767            }
1768        }
1769    }
1770
1771    fn procedure(&mut self, p: &ProcName, pos: Pos) {
1772        if let Err(m) = procedure(self.program, p) {
1773            self.errors.push(m.at(pos));
1774        }
1775    }
1776
1777    /// A reference to data: an identifier names a data item or a function, and a condition-name
1778    /// is neither (Language Reference SC27-8713-03, p. 69).
1779    fn reference(&mut self, r: &Ref) {
1780        self.reference_or_condition(r);
1781        if let Ok(layout::Resolved::Condition(_)) = self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
1782            self.errors.push(syntax::messages::IWC0095.at(r.pos, format!("{} is a condition-name, not a data item", r.name)));
1783        }
1784    }
1785
1786    /// An arithmetic statement's receiving operand is numeric or numeric-edited (Language Reference
1787    /// SC27-8713-03, the ADD, COMPUTE, DIVIDE, MULTIPLY and SUBTRACT statements); Enterprise COBOL
1788    /// discards the statement otherwise, IGYPA3146-S. CORRESPONDING never comes here: it pairs
1789    /// numeric items of its two groups.
1790    fn arithmetic_receiver(&mut self, verb: &str, r: &Ref) {
1791        let Some(i) = self.item(r) else { return };
1792        let kind = self.layout.items[i].kind;
1793        if kind.is_numeric() || matches!(kind, rt::storage::Kind::NumericEdited { .. }) {
1794            return;
1795        }
1796        let class = match kind {
1797            rt::storage::Kind::Group => "a group",
1798            rt::storage::Kind::Alnum { .. } => "alphanumeric",
1799            rt::storage::Kind::AlnumEdited { .. } => "alphanumeric-edited",
1800            rt::storage::Kind::National => "national",
1801            rt::storage::Kind::Pointer | rt::storage::Kind::ProgramPointer => "a pointer",
1802            _ => "not numeric",
1803        };
1804        self.errors.push(syntax::messages::IWC0323.at(r.pos, format!("{verb} {}: a receiving operand of an arithmetic statement must be numeric or numeric-edited, and {} is {class}", r.name, r.name)));
1805    }
1806
1807    /// A reference that may name a condition-name: a condition's, or SET TO TRUE's or FALSE's.
1808    fn reference_or_condition(&mut self, r: &Ref) {
1809        if r.name == "RETURN-CODE" && r.qualifiers.is_empty() && self.layout.resolve(&r.name, &r.qualifiers, r.pos).is_err() {
1810            return;
1811        }
1812        if oo::special_register(self.layout, r) || markup::xml_register(self.layout, r) {
1813            return;
1814        }
1815        if !self.debugging && !self.program.declaratives.debugging.is_empty() && declaratives::DEBUG_ITEM_NAMES.contains(&r.name.as_str()) {
1816            self.errors.push(syntax::messages::IWC0096.at(r.pos, format!("{}: only a debugging section may reference DEBUG-ITEM", r.name)));
1817            return;
1818        }
1819        match self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
1820            Err(e) => self.errors.push(e),
1821            Ok(layout::Resolved::Item(i)) if switches::switch_of(self.layout, i).is_some() => self.errors.push(switches::mnemonic_as_data(r, self.layout, i)),
1822            Ok(layout::Resolved::Item(i)) if self.layout.items[i].dims.len() != r.subscripts.len() => self.errors.push(syntax::messages::IWC0097.at(
1823                r.pos,
1824                format!("{} takes {} subscripts, not {}", r.name, self.layout.items[i].dims.len(), r.subscripts.len()),
1825            )),
1826            Ok(_) => {}
1827        }
1828        r.subscripts.iter().for_each(|e| self.expr(e));
1829        if let Some(rm) = &r.refmod {
1830            self.expr(&rm.start);
1831            if let Some(l) = &rm.length {
1832                self.expr(l);
1833            }
1834        }
1835    }
1836
1837    /// Every host variable and every CICS argument that names data must resolve.
1838    fn exec_block(&mut self, block: &ExecBlock) {
1839        if block.kind == ExecKind::Dli {
1840            self.dli_block(block);
1841        }
1842        if block.kind == ExecKind::Cics && matches!(block.command.as_str(), "HANDLE CONDITION" | "HANDLE AID" | "HANDLE ABEND") {
1843            self.handle_labels(block);
1844        }
1845        if let Some(syntax::sql::Sql { statement: syntax::sql::Statement::Malformed(why), .. }) = &block.sql {
1846            self.errors.push(syntax::messages::IWP0003.at(block.pos, format!("EXEC SQL {}: {why}", block.command)));
1847        }
1848        // Db2 13 requires a colon before every host variable (SQL Reference, db2z_refs2hostvars):
1849        // an INTO name without one is refused under strict and read as a host variable under extended.
1850        for r in block.sql.iter().flat_map(|sql| sql.statement.colonless_into()) {
1851            if self.extended {
1852                self.errors.push(syntax::messages::IWX0011.at(r.pos, format!("{INTO_WITHOUT_COLON}: {} is read as a host variable", r.name)));
1853            } else {
1854                self.errors.push(syntax::messages::IWP0046.at(r.pos, format!("EXEC SQL {}: {} in the INTO list has no colon, which Db2 requires before every host variable", block.command, r.name)));
1855            }
1856        }
1857        for r in &block.host_variables {
1858            if r.subscripts.is_empty() {
1859                self.reference_unsubscripted(r);
1860            } else {
1861                self.reference(r);
1862            }
1863        }
1864        for (_, arg) in &block.options {
1865            if let Some(ExecArg::Operand(op)) = arg {
1866                self.operand(op);
1867            }
1868        }
1869    }
1870
1871    /// IBM's IGYPS2047-W: with only a REPLACING phrase, a receiver none of whose elementary items is
1872    /// of a category the phrase names is not initialized (Migration Guide GC27-8715-03, p. 137).
1873    fn initialize_incompatible(&mut self, targets: &[Ref], with: &InitializeWith, pos: Pos) {
1874        if !with.value.is_empty() || with.default {
1875            return;
1876        }
1877        for r in targets {
1878            let Some(i) = self.item(r) else { continue };
1879            let initialized = match r.refmod {
1880                Some(_) => with.initial_value(Some(self.layout.refmod_category(Some(i), self.layout.items[i].kind)), false).is_some(),
1881                None => self.layout.initialize_receivers(i, with.filler).iter().any(|&(e, _)| with.initial_value(self.layout.category(e), false).is_some()),
1882            };
1883            if !initialized {
1884                let categories: Vec<&str> = with.replacing.iter().map(|(c, _)| c.word()).collect();
1885                self.errors.push(syntax::messages::IWC0098.at(
1886                    pos,
1887                    format!("INITIALIZE {}: none of its items is of a category REPLACING names ({}), so it is not initialized", r.name, categories.join(", ")),
1888                ));
1889            }
1890        }
1891    }
1892
1893    /// JSON GENERATE's and JSON PARSE's own item, which may name a whole table by leaving out its
1894    /// last subscript (Programming Guide SC27-8714-03, pp. 612-614, 619-620).
1895    fn whole_table_reference(&mut self, r: &Ref) {
1896        if let Ok(layout::Resolved::Item(i)) = self.layout.resolve(&r.name, &r.qualifiers, r.pos)
1897            && self.layout.items[i].table
1898            && self.layout.items[i].dims.len() == r.subscripts.len() + 1
1899        {
1900            r.subscripts.iter().for_each(|e| self.expr(e));
1901            return;
1902        }
1903        self.reference(r);
1904    }
1905
1906    /// An EXEC DLI command and its options against IMS's table, and each qualification's form.
1907    fn dli_block(&mut self, block: &ExecBlock) {
1908        let Some(command) = syntax::dli::find(&block.command) else {
1909            self.errors.push(syntax::messages::IWP0004.at(block.pos, format!("EXEC DLI {} is not an EXEC DLI command", block.command)));
1910            return;
1911        };
1912        for (name, arg) in &block.options {
1913            if command.options.is_some_and(|options| !options.contains(&name.as_str())) {
1914                self.errors.push(syntax::messages::IWP0005.at(block.pos, format!("EXEC DLI {}: {name} is not one of its options", command.name)));
1915            } else if let (true, Some(ExecArg::Text(t))) = (name == "WHERE", arg)
1916                && let Err(why) = syntax::dli::qualification(t)
1917            {
1918                self.errors.push(syntax::messages::IWP0006.at(block.pos, format!("EXEC DLI {} WHERE({t}): {why}", command.name)));
1919            }
1920        }
1921    }
1922
1923    /// A function's value can be inspected only as a character string, and only by TALLYING:
1924    /// REPLACING and CONVERTING store into the inspected item (Language Reference SC27-8713-03,
1925    /// pp. 77, 359; assumption C190).
1926    fn inspected_function(&mut self, f: &FunctionCall, i: &Inspect) {
1927        let name = f.name.as_str();
1928        let known = self.intrinsic(name);
1929        let numeric_udf = self.functions.into_iter().flatten().any(|u| u.name == name && !u.character_valued());
1930        if known && !rt::intrinsic::CHARACTER_VALUED.contains(&name) || numeric_udf {
1931            self.errors.push(syntax::messages::IWC0099.at(f.pos, format!("INSPECT FUNCTION {name}: an integer or numeric function can be used only where an arithmetic expression can, not as the inspected item")));
1932        }
1933        let stores = if !i.replacing.is_empty() {
1934            Some("REPLACING")
1935        } else {
1936            i.converting.as_ref().map(|_| "CONVERTING")
1937        };
1938        if let Some(phrase) = stores {
1939            self.errors.push(syntax::messages::IWC0100.at(f.pos, format!("INSPECT FUNCTION {name} {phrase}: {phrase} stores into the inspected item, and a function-identifier cannot be a receiving operand")));
1940        }
1941    }
1942
1943    /// An integer or numeric intrinsic function, MAX and MIN among them when their arguments are
1944    /// numeric, can be used only where an arithmetic expression can (Language Reference
1945    /// SC27-8713-03, p. 499; Programming Guide SC27-8714-03, p. 56), and DISPLAY's operands are
1946    /// identifiers and literals (assumption C332).
1947    fn displayed_function(&mut self, f: &FunctionCall) {
1948        let name = f.name.as_str();
1949        let numeric = !rt::intrinsic::CHARACTER_VALUED.contains(&name) || self.numeric_max_or_min(f);
1950        if self.intrinsic(name) && numeric {
1951            self.errors.push(syntax::messages::IWC0101.at(f.pos, format!("DISPLAY FUNCTION {name}: an integer or numeric function can be used only where an arithmetic expression can, and DISPLAY takes none")));
1952        }
1953    }
1954
1955    /// A DISPLAY's or ACCEPT's screen phrases, Micro Focus's and GnuCOBOL's: under `--compliance
1956    /// extended` a warning naming where on the screen, refused under strict.
1957    fn screen(&mut self, statement: &str, phrases: &ScreenPhrases) {
1958        let mut placed = Vec::new();
1959        match &phrases.at {
1960            Some(ScreenAt::Combined(o)) => placed.push(o),
1961            Some(ScreenAt::LineColumn { line, column }) => placed.extend(line.iter().chain(column)),
1962            None => {}
1963        }
1964        for o in placed {
1965            self.operand(o);
1966        }
1967        if !self.extended {
1968            self.errors.push(syntax::messages::IWC0298.at(phrases.pos, format!("{statement} on the screen: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it")));
1969            return;
1970        }
1971        let at = match &phrases.at {
1972            Some(ScreenAt::Combined(_)) => "the line and column AT gives",
1973            Some(ScreenAt::LineColumn { line: Some(_), column: Some(_) }) => "the line and column given",
1974            Some(ScreenAt::LineColumn { line: Some(_), column: None }) => "column 1 of the line given",
1975            Some(ScreenAt::LineColumn { line: None, .. }) => "the column given on the cursor's line",
1976            None => "the cursor",
1977        };
1978        self.errors.push(syntax::messages::IWX0020.at(phrases.pos, format!("{statement} on the screen (Micro Focus and GnuCOBOL; Enterprise COBOL has none): at {at}")));
1979    }
1980
1981    /// ACCEPT ... FROM COMMAND-LINE, ARGUMENT-NUMBER and ARGUMENT-VALUE are Micro Focus's and
1982    /// GnuCOBOL's, which `--compliance extended` reads from the job step's PARM with IWX0010-W
1983    /// (assumption C442); ON EXCEPTION goes with ARGUMENT-VALUE alone.
1984    fn accept_source(&mut self, from: AcceptFrom, exception: &Handlers, target: &Ref, pos: Pos) {
1985        if (exception.on.is_some() || exception.not_on.is_some()) && !matches!(from, AcceptFrom::ArgumentValue | AcceptFrom::EnvironmentValue) {
1986            self.errors.push(syntax::messages::IWC0256.at(pos, "ACCEPT ... ON EXCEPTION: of the ACCEPT statements, only ACCEPT ... FROM ARGUMENT-VALUE and the environment under --compliance extended have an exception"));
1987        }
1988        if from == AcceptFrom::EnvironmentValue {
1989            let named = self.environment_named.take() == Some(pos);
1990            let written = if named { "ACCEPT ... FROM ENVIRONMENT" } else { "ACCEPT ... FROM ENVIRONMENT-VALUE" };
1991            if self.extended {
1992                self.errors.push(syntax::messages::IWX0021.at(pos, format!("{written} (Micro Focus and GnuCOBOL; Enterprise COBOL reads and sets no environment variable): {} receives the value of the environment variable named, or spaces and the exception when it is not set", target.name)));
1993            } else if named {
1994                self.errors.push(syntax::messages::IWS0055.at(pos, "ACCEPT ... FROM ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's"));
1995            } else {
1996                self.errors.push(syntax::messages::IWS0060.at(pos, "ACCEPT ... FROM ENVIRONMENT-VALUE: GnuCOBOL's, not Enterprise COBOL's"));
1997            }
1998            return;
1999        }
2000        let (name, what) = match from {
2001            AcceptFrom::CommandLine => ("COMMAND-LINE", "the job step's PARM program arguments"),
2002            AcceptFrom::ArgumentNumber => ("ARGUMENT-NUMBER", "how many words the job step's PARM program arguments hold"),
2003            AcceptFrom::ArgumentValue => ("ARGUMENT-VALUE", "the next word of the job step's PARM program arguments"),
2004            _ => return,
2005        };
2006        if self.extended {
2007            self.errors.push(syntax::messages::IWX0010.at(pos, format!("ACCEPT ... FROM {name} (Micro Focus and GnuCOBOL; Enterprise COBOL reads no command line): {} receives {what}", target.name)));
2008        } else {
2009            self.errors.push(syntax::messages::IWC0257.at(pos, format!("ACCEPT ... FROM {name}: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it from the job step's PARM")));
2010        }
2011    }
2012
2013    /// DISPLAY UPON ENVIRONMENT-NAME or ENVIRONMENT-VALUE, written or standing for ACCEPT ... FROM
2014    /// ENVIRONMENT's naming or SET ENVIRONMENT, Micro Focus's and GnuCOBOL's: under `--compliance
2015    /// extended` a warning saying what it does, refused under strict. ACCEPT ... FROM
2016    /// ENVIRONMENT's message is given at its ACCEPT, and SET ENVIRONMENT's at its value.
2017    fn environment_display(&mut self, upon: &Upon, pos: Pos) {
2018        let value = upon.device == "ENVIRONMENT-VALUE";
2019        match upon.name.as_str() {
2020            "ENVIRONMENT" => self.environment_named = Some(pos),
2021            "SET ENVIRONMENT" if !value => {}
2022            "SET ENVIRONMENT" if self.extended => self.errors.push(syntax::messages::IWX0021.at(pos, "SET ENVIRONMENT (Micro Focus and GnuCOBOL; Enterprise COBOL reads and sets no environment variable): the environment variable named takes the value")),
2023            "SET ENVIRONMENT" => self.errors.push(syntax::messages::IWS0061.at(pos, "SET ENVIRONMENT is GnuCOBOL's, not Enterprise COBOL's")),
2024            _ if self.extended => {
2025                let what = if value { "the environment variable ENVIRONMENT-NAME last named takes the value" } else { "names the environment variable ENVIRONMENT-VALUE reads and sets next" };
2026                self.errors.push(syntax::messages::IWX0021.at(pos, format!("DISPLAY UPON {} (Micro Focus and GnuCOBOL; Enterprise COBOL reads and sets no environment variable): {what}", upon.device)));
2027            }
2028            _ => self.errors.push(syntax::messages::IWC0073.at(pos, format!("DISPLAY UPON {}: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it", upon.name))),
2029        }
2030    }
2031
2032    /// DISPLAY ... UPON ARGUMENT-NUMBER, Micro Focus's and GnuCOBOL's, which under `--compliance
2033    /// extended` makes the numeric item or literal it shows the number of the PARM word the next
2034    /// ACCEPT ... FROM ARGUMENT-VALUE takes, with IWX0010-W (assumption C442).
2035    fn argument_number(&mut self, items: &[Operand], pos: Pos) {
2036        if !self.extended {
2037            self.errors.push(syntax::messages::IWC0258.at(pos, "DISPLAY UPON ARGUMENT-NUMBER: Micro Focus's and GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it"));
2038            return;
2039        }
2040        let numeric = match items {
2041            [Operand::Literal(Literal::Number(_))] => true,
2042            [Operand::Ref(r)] => self.item(r).is_some_and(|i| self.layout.items[i].kind.is_numeric()),
2043            _ => false,
2044        };
2045        if numeric {
2046            self.errors.push(syntax::messages::IWX0010.at(pos, "DISPLAY ... UPON ARGUMENT-NUMBER (Micro Focus and GnuCOBOL; Enterprise COBOL reads no command line): the next ACCEPT ... FROM ARGUMENT-VALUE takes the job step's PARM word it numbers"));
2047        } else {
2048            self.errors.push(syntax::messages::IWC0259.at(pos, "DISPLAY UPON ARGUMENT-NUMBER: it shows one numeric item or literal, the number of the argument the next ACCEPT ... FROM ARGUMENT-VALUE takes"));
2049        }
2050    }
2051
2052    /// An integer or numeric intrinsic function, MAX and MIN among them when their arguments are
2053    /// numeric, is no MOVE sender: it can be used only where an arithmetic expression can, and
2054    /// MOVE's sender is an identifier or literal (Language Reference SC27-8713-03, pp. 400-402, 499;
2055    /// Programming Guide SC27-8714-03, p. 119; assumption C394). `--compliance extended` moves its
2056    /// value with IWX0008-W.
2057    fn moved_function(&mut self, f: &FunctionCall) {
2058        let name = f.name.as_str();
2059        let numeric = !rt::intrinsic::CHARACTER_VALUED.contains(&name) || self.numeric_max_or_min(f);
2060        if !self.intrinsic(name) || !numeric {
2061            return;
2062        }
2063        if self.extended {
2064            self.errors.push(syntax::messages::IWX0008.at(f.pos, format!("{NUMERIC_FUNCTION_MOVED}: FUNCTION {name} is moved as its value")));
2065        } else {
2066            self.errors.push(syntax::messages::IWC0102.at(f.pos, format!("MOVE FUNCTION {name}: an integer or numeric function can be used only where an arithmetic expression can, not as a MOVE's sender")));
2067        }
2068    }
2069
2070    /// INSPECT's identifiers but the count field have the inspected item's usage, and its literals
2071    /// are national when it is national, DBCS when it is DBCS and alphanumeric otherwise; a
2072    /// figurative constant is any of them (Language Reference SC27-8713-03, p. 355; assumption
2073    /// C231). A function-identifier's category is known only when it runs.
2074    fn inspected_usage(&mut self, target: &Ref, i: &Inspect) {
2075        let Some(t) = self.item(target) else { return };
2076        let usage = CharUsage::of(self.layout.items[t].kind);
2077        let phrases = i.tallying.iter().chain(&i.replacing).flat_map(|p| p.pattern.iter().chain(&p.by).chain(p.bounds.iter().map(|b| &b.value)));
2078        let converting = i.converting.iter().flat_map(|(from, to, bounds)| [from, to].into_iter().chain(bounds.iter().map(|b| &b.value)));
2079        for op in phrases.chain(converting) {
2080            let (operand, operand_usage, pos) = match op {
2081                Operand::Literal(l) => match inspected_literal(l) {
2082                    Some((what, operand_usage)) => (what.to_owned(), operand_usage, i.pos),
2083                    None => continue,
2084                },
2085                Operand::Ref(r) => match self.item(r) {
2086                    Some(k) => (r.name.clone(), CharUsage::of(self.layout.items[k].kind), r.pos),
2087                    None => continue,
2088                },
2089                _ => continue,
2090            };
2091            let name = &target.name;
2092            let why = match (usage, operand_usage) {
2093                (u, o) if u == o => continue,
2094                (CharUsage::Display, o) => format!("{name} is not {o}, and an operand can be {o} only when the inspected item is"),
2095                (u, _) => format!("{name} is {u} and every operand but the count field must be {u} too"),
2096            };
2097            self.errors.push(syntax::messages::IWC0103.at(pos, format!("INSPECT {name}: {operand} cannot be an operand here, since {why}")));
2098        }
2099    }
2100
2101    /// A HANDLE label is a paragraph or section of the program, where control goes when the
2102    /// condition, attention key or abend comes (Programming Guide SC27-8714-03, p. 503).
2103    fn handle_labels(&mut self, block: &ExecBlock) {
2104        let abend = block.command == "HANDLE ABEND";
2105        for (option, arg) in &block.options {
2106            let Some(ExecArg::Text(label)) = arg else { continue };
2107            let label = label.trim();
2108            if label.is_empty() || matches!(option.as_str(), "RESP" | "RESP2" | "NOHANDLE") || abend && option != "LABEL" {
2109                continue;
2110            }
2111            let name = ProcName { name: label.to_ascii_uppercase(), section: None };
2112            if let Err(m) = procedure_from(self.program, &name, self.paragraph) {
2113                self.errors.push(syntax::messages::IWP0007.at(block.pos, format!("EXEC CICS {} {option}({label}): {m}", block.command)));
2114            }
2115        }
2116    }
2117
2118    /// SEARCH VARYING names an index-name, an index data item or an elementary integer item
2119    /// (Language Reference SC27-8713-03, p. 437).
2120    fn search_varying(&mut self, se: &Search, v: &Ref) {
2121        use rt::storage::Kind;
2122        let Some(i) = self.item(v) else { return };
2123        if !matches!(self.layout.items[i].kind, Kind::Index | Kind::Zoned { scale: 0, .. } | Kind::Packed { scale: 0, .. } | Kind::Binary { scale: 0, .. }) {
2124            self.errors.push(syntax::messages::IWC0104.at(v.pos, format!("SEARCH {} VARYING {}: not an index-name, an index data item or an elementary integer item", se.table.name, v.name)));
2125        }
2126    }
2127
2128    /// SEARCH names a table without a subscript.
2129    fn reference_unsubscripted(&mut self, r: &Ref) {
2130        if let Err(e) = self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
2131            self.errors.push(e);
2132        }
2133    }
2134
2135    /// Whether FUNCTION `name` invokes an intrinsic function: a user-defined function this program
2136    /// may invoke takes the name over.
2137    fn intrinsic(&self, name: &str) -> bool {
2138        (FUNCTIONS.contains(&name) || rt::intrinsic::FUNCTIONS.contains(&name)) && !self.functions.into_iter().flatten().any(|u| u.name == name)
2139    }
2140
2141    fn operand(&mut self, op: &Operand) {
2142        match op {
2143            Operand::LengthOf(r) => self.reference(&self.layout.length_of_ref(r)),
2144            Operand::Ref(r) | Operand::AddressOf(r) => self.reference(r),
2145            Operand::Literal(Literal::Number(t)) if literal_fixed(t).is_none() || literal_digits(t) > self.max_digits as usize => {
2146                self.errors.push(syntax::messages::IWC0105.at(Pos::default(), format!("the literal {t} has more than {} digits", self.max_digits.min(31))));
2147            }
2148            Operand::Literal(_) => {}
2149            Operand::Function(f) => {
2150                if f.name == "MODULE-CALLER-ID" && self.intrinsic(&f.name) {
2151                    self.errors.push(if self.extended {
2152                        syntax::messages::IWX0031.at(f.pos, "FUNCTION MODULE-CALLER-ID (GnuCOBOL; Enterprise COBOL has no such function): the PROGRAM-ID of the program that called this one, empty in the main program")
2153                    } else {
2154                        syntax::messages::IWC0305.at(f.pos, "FUNCTION MODULE-CALLER-ID: GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it")
2155                    });
2156                }
2157                if f.name == "CONCATENATE" && self.intrinsic(&f.name) {
2158                    self.errors.push(if self.extended {
2159                        syntax::messages::IWX0053.at(f.pos, "FUNCTION CONCATENATE (GnuCOBOL; Enterprise COBOL has no such function): its arguments' characters joined, a number's as its digits")
2160                    } else {
2161                        syntax::messages::IWC0319.at(f.pos, "FUNCTION CONCATENATE: GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it")
2162                    });
2163                }
2164                if f.name == "STORED-CHAR-LENGTH" && self.intrinsic(&f.name) {
2165                    self.errors.push(if self.extended {
2166                        syntax::messages::IWX0042.at(f.pos, "FUNCTION STORED-CHAR-LENGTH (GnuCOBOL; Enterprise COBOL has no such function): the argument's length in characters without its trailing spaces")
2167                    } else {
2168                        syntax::messages::IWC0312.at(f.pos, "FUNCTION STORED-CHAR-LENGTH: GnuCOBOL's, not Enterprise COBOL's; --compliance extended reads it")
2169                    });
2170                }
2171                if !self.intrinsic(&f.name) {
2172                    match self.functions.map(|all| all.iter().find(|u| u.name == f.name)) {
2173                        Some(Some(udf)) => function::check_invocation(udf, f, self.layout, self.alphabetic, self.program.environment.decimal_point_comma, self.errors),
2174                        Some(None) => self.errors.push(syntax::messages::IWC0106.at(f.pos, format!("FUNCTION {}: neither an intrinsic function nor a user-defined function defined or prototyped before this program", f.name))),
2175                        None => self.errors.push(syntax::messages::IWR0075.at(f.pos, format!("FUNCTION {}: a user-defined function is not supported here yet", f.name))),
2176                    }
2177                }
2178                self.function_arguments(f);
2179                f.args.iter().for_each(|a| self.expr(a));
2180            }
2181        }
2182    }
2183
2184    fn expr(&mut self, e: &Expr) {
2185        match e {
2186            Expr::Operand(op) => self.operand(op),
2187            Expr::Neg(inner) => self.expr(inner),
2188            Expr::Bin(a, _, b) => {
2189                self.expr(a);
2190                self.expr(b);
2191            }
2192        }
2193    }
2194
2195    fn cond(&mut self, c: &Cond) {
2196        match c {
2197            Cond::Rel(a, _, b) => {
2198                self.expr(a);
2199                self.expr(b);
2200                self.comparison(a, b);
2201            }
2202            Cond::Class(e, class) => {
2203                self.expr(e);
2204                if let Class::Named(name) = class {
2205                    self.class_name_subject(e, name);
2206                }
2207            }
2208            Cond::Name(r) => self.reference_or_condition(r),
2209            Cond::NameOrRel { subject, name, .. } => {
2210                self.expr(subject);
2211                self.reference_or_condition(name);
2212                if self.item(name).is_some() {
2213                    self.comparison(subject, &Expr::Operand(Operand::Ref(name.clone())));
2214                }
2215            }
2216            Cond::Not(inner) => self.cond(inner),
2217            Cond::And(a, b) | Cond::Or(a, b) => {
2218                self.cond(a);
2219                self.cond(b);
2220            }
2221        }
2222    }
2223}
2224
2225#[cfg(test)]
2226mod tests {
2227    use super::*;
2228    use std::ffi::OsStr;
2229    use std::time::Duration;
2230
2231    #[test]
2232    fn source_date_epoch_decides_the_compile_time_and_the_clock_stands_in_for_it() {
2233        let clock = Duration::from_millis(1_790_510_400_428);
2234        let from = |epoch: Option<&str>| compile_time_from(epoch.map(OsStr::new), clock);
2235        assert_eq!(from(None), Ok(CompileTime { seconds: 1_790_510_400, hundredths: 42, source: TimeSource::Clock }));
2236        assert_eq!(from(Some("315532800")), Ok(CompileTime { seconds: 315_532_800, hundredths: 0, source: TimeSource::SourceDateEpoch }));
2237        assert_eq!(from(Some("253402300799")).map(|t| t.seconds), Ok(CompileTime::LATEST));
2238        for bad in ["", "-1", "+5", "1.5", " 7", "253402300800"] {
2239            assert!(from(Some(bad)).unwrap_err().starts_with(&format!("SOURCE_DATE_EPOCH={bad}: ")), "{bad}");
2240        }
2241    }
2242}