Skip to main content

ironwork_compile/
lib.rs

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