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