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