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