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