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