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