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