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