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