1use crate::layout::{self, Layout};
9use crate::picture::{self, Category};
10use syntax::ast::*;
11use syntax::report::{self as rw, ColumnNumber, ControlName, Footing, GroupType, LineNumber, NextGroup, ReportStmt};
12use syntax::{Error, Pos};
13
14pub mod state {
16 pub const INITIATED: usize = 0;
17 pub const GENERATED: usize = 1;
18 pub const STARTED: usize = 2;
20 pub const HEADING_DUE: usize = 3;
22 pub const BODY_ON_PAGE: usize = 4;
23 pub const HEADING_ONLY: usize = 5;
25 pub const VERTICAL: usize = 8;
27 pub const SAVED_NEXT_GROUP: usize = 12;
29 pub const FLAGS: usize = 16;
31}
32
33#[derive(Clone, Debug, Default)]
35pub struct Writer {
36 pub reports: Vec<Report>,
37 pub print_switch: Option<usize>,
39}
40
41#[derive(Clone, Debug)]
42pub struct Report {
43 pub name: String,
44 pub file: usize,
45 pub code: Option<Literal>,
46 pub width: usize,
48 pub page: Option<Page>,
49 pub controls: Vec<Control>,
51 pub groups: Vec<Group>,
52 pub sums: Vec<Sum>,
53 pub subtotals: Vec<Subtotal>,
55 pub page_counter: usize,
56 pub line_counter: usize,
57 pub state: usize,
58 pub report_heading: Option<usize>,
59 pub page_heading: Option<usize>,
60 pub page_footing: Option<usize>,
61 pub report_footing: Option<usize>,
62 pub control_headings: Vec<Option<usize>>,
64 pub control_footings: Vec<Option<usize>>,
65 pub first_detail_written: Option<i64>,
67}
68
69#[derive(Clone, Copy, Debug)]
71pub struct Page {
72 pub limit: i64,
73 pub heading: i64,
74 pub first_detail: i64,
75 pub last_detail: i64,
76 pub footing: i64,
77}
78
79#[derive(Clone, Debug)]
80pub struct Control {
81 pub reference: Ref,
82 pub saved: usize,
84 pub len: usize,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum GroupKind {
89 ReportHeading,
90 PageHeading,
91 ControlHeading,
92 Detail,
93 ControlFooting,
94 PageFooting,
95 ReportFooting,
96}
97
98impl GroupKind {
99 pub fn is_body(self) -> bool {
100 matches!(self, Self::ControlHeading | Self::Detail | Self::ControlFooting)
101 }
102}
103
104#[derive(Clone, Debug)]
105pub struct Group {
106 pub name: Option<String>,
107 pub kind: GroupKind,
108 pub level: usize,
110 pub next_group: Option<NextGroup>,
111 pub lines: Vec<Line>,
112 pub unprinted: Vec<Field>,
114 pub cross: Vec<(usize, Origin)>,
116 pub rolls: Vec<(usize, Origin)>,
118 pub totals: Vec<usize>,
120 pub indicate: Option<usize>,
121 pub declarative: Option<(usize, usize)>,
123}
124
125#[derive(Clone, Debug)]
126pub struct Line {
127 pub number: LineNumber,
128 pub fields: Vec<Field>,
129}
130
131#[derive(Clone, Debug)]
132pub struct Field {
133 pub item: usize,
134 pub column: usize,
136 pub content: FieldContent,
137 pub group_indicate: bool,
138 pub blank_when_zero: bool,
140 pub rounded: bool,
141 pub pos: Pos,
142}
143
144#[derive(Clone, Debug)]
145pub enum FieldContent {
146 Source(Expr),
147 Value(Literal),
148 Sum(usize),
149 Program,
151}
152
153#[derive(Clone, Debug)]
154pub struct Sum {
155 pub total: usize,
156 pub reset: Option<usize>,
158}
159
160#[derive(Clone, Debug)]
162pub enum Origin {
163 Source(Expr),
164 Value(Literal),
165 Total(usize),
166}
167
168#[derive(Clone, Debug)]
169pub struct Subtotal {
170 pub sum: usize,
171 pub operand: Expr,
172 pub adding: Adding,
173}
174
175#[derive(Clone, Debug)]
176pub enum Adding {
177 EveryGenerate,
178 Upon(Vec<usize>),
180 Correlated(Vec<usize>),
182}
183
184pub(crate) struct Draft {
186 file: Option<usize>,
187 width: usize,
188 children: usize,
190 groups: Vec<DraftGroup>,
191 controls: Vec<(usize, usize)>,
192}
193
194struct DraftGroup {
195 lines: Vec<(LineNumber, Vec<DraftField>)>,
196 unprinted: Vec<DraftField>,
197 totals: Vec<(usize, usize)>,
199}
200
201struct DraftField {
202 entry: usize,
203 child: usize,
204 column: usize,
205 size: usize,
206 blank_when_zero: bool,
207 group_indicate: bool,
208 category: Category,
209}
210
211pub(crate) fn entry(level: u8, name: Option<String>, picture: Option<String>, usage: Option<Usage>, pos: Pos) -> DataEntry {
212 DataEntry {
213 level,
214 name,
215 picture,
216 usage,
217 value: None,
218 redefines: None,
219 occurs: None,
220 depending_on: None,
221 sign: None,
222 justified: false,
223 sync: false,
224 blank_when_zero: false,
225 indexed_by: Vec::new(),
226 keys: Vec::new(),
227 condition_values: Vec::new(),
228 false_value: None,
229 renames: None,
230 object_class: None,
231 pos,
232 }
233}
234
235fn implied_picture(lit: &Literal) -> Option<String> {
238 Some(match lit {
239 Literal::Alnum(s) if !s.is_empty() => format!("X({})", s.chars().count()),
240 Literal::Hex(b) if !b.is_empty() => format!("X({})", b.len()),
241 Literal::National(s) if !s.is_empty() => format!("N({})", s.chars().count()),
242 Literal::Number(t) => {
243 let body = t.trim_start_matches(['+', '-']);
244 let (int, frac) = body.split_once('.').unwrap_or((body, ""));
245 let int = int.len().max(1);
246 if frac.is_empty() { format!("S9({int})") } else { format!("S9({int})V9({})", frac.len()) }
247 }
248 _ => return None,
249 })
250}
251
252fn entry_picture(e: &rw::Entry) -> Option<String> {
253 e.picture.clone().or_else(|| match &e.content {
254 Some(rw::Content::Value(lit)) => implied_picture(lit),
255 _ => None,
256 })
257}
258
259fn places(picture: &str, decimal_comma: bool) -> Option<(u32, u32)> {
261 let p = picture::analyse_with(picture, decimal_comma).ok()?;
262 matches!(p.category, Category::Numeric | Category::NumericEdited).then_some(((p.digits + p.scaling).saturating_sub(p.scale), p.scale))
263}
264
265fn report_entry(reports: &[rw::Report], current: usize, operand: &Ref) -> Option<(usize, usize, usize)> {
267 let report = match operand.qualifiers.as_slice() {
268 [] => current,
269 [q] => reports.iter().position(|r| r.name == *q)?,
270 _ => return None,
271 };
272 if !operand.subscripts.is_empty() || operand.refmod.is_some() {
273 return None;
274 }
275 reports[report].groups.iter().enumerate().find_map(|(g, group)| {
276 group.entries.iter().position(|e| e.name.as_deref() == Some(operand.name.as_str()) && entry_picture(e).is_some()).map(|e| (report, g, e))
277 })
278}
279
280pub(crate) fn prepare(program: &mut Program, adv: bool, errors: &mut Vec<Error>) -> Vec<Draft> {
284 let reports = program.report_writer.reports.clone();
285 if reports.is_empty() {
286 return Vec::new();
287 }
288 let control_sizes = measure_controls(program, &reports);
289 let taken: std::collections::HashSet<String> = program
290 .working_storage
291 .iter()
292 .chain(program.files.iter().flat_map(|f| &f.records))
293 .chain(&program.linkage)
294 .chain(&program.local_storage)
295 .filter_map(|e| e.name.clone())
296 .collect();
297 let mut drafts = Vec::new();
298 let mut added = Vec::new();
299 for (ri, r) in reports.iter().enumerate() {
300 let holders: Vec<usize> = program.files.iter().enumerate().filter(|(_, f)| f.reports.contains(&r.name)).map(|(k, _)| k).collect();
301 let file = match holders.as_slice() {
302 [k] => Some(*k),
303 [] => {
304 errors.push(Error::at(r.pos, format!("report {} is named in no FD's REPORT clause", r.name)));
305 None
306 }
307 _ => {
308 errors.push(Error::at(r.pos, format!("report {} in more than one FD (INITIATE ... UPON) is not supported yet", r.name)));
309 None
310 }
311 };
312 let mut children = vec![
313 entry(5, Some("PAGE-COUNTER".into()), Some("S9(9)".into()), Some(Usage::Binary), r.pos),
314 entry(5, Some("LINE-COUNTER".into()), Some("S9(9)".into()), Some(Usage::Binary), r.pos),
315 entry(5, None, None, None, r.pos),
316 ];
317 let mut groups = Vec::new();
318 for g in &r.groups {
319 groups.push(draft_group(&reports, ri, g, &taken, &mut children, program.environment.decimal_point_comma, errors));
320 }
321 let mut controls = Vec::new();
322 let mut cursor = state::FLAGS + r.groups.len();
323 for &len in &control_sizes[ri] {
324 controls.push((cursor, len));
325 cursor += len;
326 }
327 children[2].picture = Some(format!("X({cursor})"));
328 added.push(entry(1, Some(r.name.clone()), None, None, r.pos));
329 let count = children.len();
330 added.extend(children);
331 drafts.push(Draft { file, width: 0, children: count, groups, controls });
332 }
333 for (k, f) in program.files.iter_mut().enumerate() {
334 let mine: Vec<usize> = drafts.iter().enumerate().filter(|(_, d)| d.file == Some(k)).map(|(i, _)| i).collect();
335 if mine.is_empty() {
336 continue;
337 }
338 let reserved = usize::from(crate::printer::reserves_first_byte(f, adv));
339 let code = |ri: usize| reserved + code_bytes(&reports[ri].code);
340 let longest = mine.iter().map(|&ri| line_end(&drafts[ri])).max().unwrap_or(0);
341 let record = match f.record_max {
342 Some(n) => n as usize,
343 None => {
344 let n = (longest.div_ceil(4) * 4).max(4) + mine.iter().map(|&ri| code(ri)).max().unwrap_or(0);
345 f.record_max = Some(n as u32);
346 if f.recording != Some('V') {
347 f.record_min.get_or_insert(n as u32);
348 }
349 n
350 }
351 };
352 for &ri in &mine {
353 let width = record.saturating_sub(code(ri));
354 drafts[ri].width = width;
355 let end = line_end(&drafts[ri]);
356 if end > width {
357 errors.push(Error::at(reports[ri].pos, format!("report {}: a line reaches column {end}, beyond the {width} bytes of the report file's record", reports[ri].name)));
358 }
359 if let Some(limit) = reports[ri].line_limit.filter(|&l| end > l as usize) {
360 errors.push(Error::at(reports[ri].pos, format!("report {}: a line reaches column {end}, beyond LINE LIMIT {limit}", reports[ri].name)));
361 }
362 }
363 }
364 if !taken.contains("PRINT-SWITCH") {
365 added.push(entry(1, Some("PRINT-SWITCH".into()), Some("S9(9)".into()), Some(Usage::Binary), Pos::default()));
366 }
367 program.working_storage.extend(added);
368 drafts
369}
370
371fn code_bytes(code: &Option<Literal>) -> usize {
372 match code {
373 Some(Literal::Alnum(s)) => s.chars().count(),
374 Some(Literal::Hex(b)) => b.len(),
375 _ => 0,
376 }
377}
378
379fn line_end(d: &Draft) -> usize {
381 d.groups.iter().flat_map(|g| &g.lines).flat_map(|l| &l.1).map(|f| f.column + f.size).max().unwrap_or(0)
382}
383
384fn measure_controls(program: &Program, reports: &[rw::Report]) -> Vec<Vec<usize>> {
386 let files: Vec<(&[DataEntry], Option<u32>)> = program.files.iter().map(|f| (f.records.as_slice(), f.record_max)).collect();
387 let built = layout::build(&program.working_storage, &files, &[], &program.linkage, &program.local_storage, program.environment.decimal_point_comma).ok();
388 reports
389 .iter()
390 .map(|r| {
391 r.controls
392 .iter()
393 .map(|c| match built.as_ref().map(|l| l.resolve(&c.name, &c.qualifiers, c.pos)) {
394 Some(Ok(layout::Resolved::Item(i))) => built.as_ref().map_or(0, |l| l.items[i].size as usize),
395 _ => 0,
396 })
397 .collect()
398 })
399 .collect()
400}
401
402fn draft_group(
405 reports: &[rw::Report],
406 ri: usize,
407 g: &rw::Group,
408 taken: &std::collections::HashSet<String>,
409 children: &mut Vec<DataEntry>,
410 decimal_comma: bool,
411 errors: &mut Vec<Error>,
412) -> DraftGroup {
413 let mut d = DraftGroup { lines: Vec::new(), unprinted: Vec::new(), totals: Vec::new() };
414 let mut inherited: Vec<(u8, bool, bool, bool)> = Vec::new();
415 let mut last_column = 0usize;
416 for (ei, e) in g.entries.iter().enumerate() {
417 while inherited.last().is_some_and(|&(level, ..)| level >= e.level) {
418 inherited.pop();
419 }
420 let (gi, bwz, just) = inherited.last().map_or((false, false, false), |&(_, a, b, c)| (a, b, c));
421 let (gi, bwz, just) = (gi || e.group_indicate, bwz || e.blank_when_zero, just || e.justified);
422 if let Some(number) = e.line {
423 let merged = matches!((d.lines.last().map(|l| l.0), number), (Some(LineNumber::Line(a)), LineNumber::Line(b)) if a == b);
424 if !merged {
425 d.lines.push((number, Vec::new()));
426 last_column = 0;
427 }
428 }
429 let elementary = g.entries.get(ei + 1).is_none_or(|next| next.level <= e.level);
430 if !elementary {
431 if e.column.is_some() || e.picture.is_some() || e.content.is_some() {
432 errors.push(Error::at(e.pos, "a group entry in a report group cannot have COLUMN, PICTURE, SOURCE, VALUE or SUM"));
433 }
434 inherited.push((e.level, gi, bwz, just));
435 continue;
436 }
437 let Some(picture) = entry_picture(e) else {
438 match (&e.content, e.column) {
439 (Some(rw::Content::Sum(_)), _) => errors.push(Error::at(e.pos, "a SUM entry needs a PICTURE")),
440 (Some(rw::Content::Value(_)), _) => errors.push(Error::at(e.pos, "a VALUE entry with a figurative constant or ALL needs a PICTURE")),
441 (Some(_), Some(_)) => errors.push(Error::at(e.pos, "a printed SOURCE entry needs a PICTURE")),
442 (None, Some(column)) => {
443 let start = column_start(column, last_column, 1, e.pos, errors);
444 last_column = start;
445 }
446 _ => {}
447 }
448 continue;
449 };
450 let analysed = match picture::analyse_with(&picture, decimal_comma) {
451 Ok(p) => p,
452 Err(m) => {
453 errors.push(Error::at(e.pos, m));
454 continue;
455 }
456 };
457 if e.content.is_none() && e.name.is_none() {
458 errors.push(Error::at(e.pos, "a report entry with no SOURCE, VALUE or SUM needs a data-name for the program to fill it"));
459 continue;
460 }
461 let size = (analysed.size as usize).max(1);
462 let numeric_edited = analysed.category == Category::NumericEdited;
463 let numeric = numeric_edited || analysed.category == Category::Numeric;
464 if e.blank_when_zero && !numeric {
465 errors.push(Error::at(e.pos, "BLANK WHEN ZERO needs a numeric PICTURE"));
466 }
467 let mut field = entry(5, None, Some(picture.clone()), Some(Usage::Display), e.pos);
468 field.sign = e.sign;
469 field.justified = just && analysed.category == Category::Alphanumeric;
470 field.blank_when_zero = bwz && numeric_edited;
471 if let Some(rw::Content::Sum(clauses)) = &e.content {
472 let Some((mut int, mut dec)) = places(&picture, decimal_comma) else {
473 errors.push(Error::at(e.pos, "a SUM entry needs a numeric PICTURE"));
474 continue;
475 };
476 for operand in clauses.iter().flat_map(|c| &c.operands) {
477 if let Some((r, og, oe)) = report_entry(reports, ri, operand)
478 && let Some((i, s)) = entry_picture(&reports[r].groups[og].entries[oe]).as_deref().and_then(|p| places(p, decimal_comma))
479 {
480 int = int.max(i);
481 dec = dec.max(s);
482 }
483 }
484 let int = int.max(1).min(31 - dec.min(30));
485 let total_picture = if dec == 0 { format!("S9({int})") } else { format!("S9({int})V9({dec})") };
486 let usage = if int + dec <= 18 { Usage::Binary } else { Usage::Packed };
487 d.totals.push((ei, children.len()));
488 children.push(entry(5, e.name.clone(), Some(total_picture), Some(usage), e.pos));
489 } else if e.name.as_ref().is_some_and(|n| e.content.is_none() || !taken.contains(n)) {
490 field.name = e.name.clone();
491 }
492 let child = children.len();
493 let draft = DraftField { entry: ei, child, column: 0, size, blank_when_zero: bwz && numeric && !numeric_edited, group_indicate: gi, category: analysed.category };
494 children.push(field);
495 match e.column {
496 Some(column) => {
497 let start = column_start(column, last_column, size, e.pos, errors);
498 last_column = start + size - 1;
499 match d.lines.last_mut() {
500 Some((_, fields)) => fields.push(DraftField { column: start.saturating_sub(1), ..draft }),
501 None => errors.push(Error::at(e.pos, "a COLUMN with no LINE above it")),
502 }
503 }
504 None => d.unprinted.push(draft),
505 }
506 }
507 d
508}
509
510fn column_start(column: ColumnNumber, last: usize, size: usize, pos: Pos, errors: &mut Vec<Error>) -> usize {
512 let start = match column {
513 ColumnNumber::Left(n) => n as i64,
514 ColumnNumber::Plus(n) => last as i64 + n as i64,
515 ColumnNumber::Right(n) => n as i64 - size as i64 + 1,
516 ColumnNumber::Center(n) => n as i64 - (size as i64 - 1) / 2,
517 };
518 if start < 1 {
519 errors.push(Error::at(pos, "a report field that starts left of column 1"));
520 return 1;
521 }
522 start as usize
523}
524
525fn control_level(controls: &[Ref], name: &ControlName) -> Option<usize> {
526 match name {
527 ControlName::Final => Some(0),
528 ControlName::Item(r) => controls.iter().position(|c| c.name == r.name && c.qualifiers == r.qualifiers && c.subscripts.is_empty()).map(|i| i + 1),
529 }
530}
531
532fn qualified(e: &Expr, report: &str) -> Expr {
534 match e {
535 Expr::Operand(op) => Expr::Operand(qualified_operand(op, report)),
536 Expr::Neg(x) => Expr::Neg(Box::new(qualified(x, report))),
537 Expr::Bin(a, op, b) => Expr::Bin(Box::new(qualified(a, report)), *op, Box::new(qualified(b, report))),
538 }
539}
540
541fn qualified_operand(op: &Operand, report: &str) -> Operand {
542 match op {
543 Operand::Ref(r) => Operand::Ref(qualified_ref(r, report)),
544 Operand::LengthOf(r) => Operand::LengthOf(qualified_ref(r, report)),
545 Operand::AddressOf(r) => Operand::AddressOf(qualified_ref(r, report)),
546 Operand::Function(f) => {
547 let mut f = f.clone();
548 f.args = f.args.iter().map(|a| qualified(a, report)).collect();
549 Operand::Function(f)
550 }
551 Operand::Literal(_) => op.clone(),
552 }
553}
554
555fn qualified_ref(r: &Ref, report: &str) -> Ref {
556 let mut r = r.clone();
557 if matches!(r.name.as_str(), "PAGE-COUNTER" | "LINE-COUNTER") && r.qualifiers.is_empty() {
558 r.qualifiers.push(report.to_owned());
559 }
560 r.subscripts = r.subscripts.iter().map(|s| qualified(s, report)).collect();
561 r
562}
563
564fn ref_text(r: &Ref) -> String {
566 let mut s = r.name.clone();
567 for q in &r.qualifiers {
568 s.push_str(" OF ");
569 s.push_str(q);
570 }
571 for e in &r.subscripts {
572 s.push(' ');
573 s.push_str(&expr_text(e));
574 }
575 s
576}
577
578fn expr_text(e: &Expr) -> String {
579 match e {
580 Expr::Operand(Operand::Ref(r)) => ref_text(r),
581 Expr::Operand(Operand::Literal(l)) => format!("{l:?}"),
582 Expr::Operand(other) => format!("{other:?}"),
583 Expr::Neg(x) => format!("-({})", expr_text(x)),
584 Expr::Bin(a, op, b) => format!("({} {op:?} {})", expr_text(a), expr_text(b)),
585 }
586}
587
588pub(crate) fn resolve(program: &Program, layout: &Layout, drafts: Vec<Draft>, errors: &mut Vec<Error>) -> Writer {
590 let reports = &program.report_writer.reports;
591 let mut writer = Writer::default();
592 if reports.is_empty() {
593 for u in &program.report_writer.uses {
594 errors.push(Error::at(u.pos, format!("USE BEFORE REPORTING {}: the program has no REPORT SECTION", u.group)));
595 }
596 return writer;
597 }
598 writer.print_switch = (0..layout.items.len()).rev().find(|&i| layout.items[i].parent.is_none() && layout.items[i].name.as_deref() == Some("PRINT-SWITCH"));
599 for (ri, (r, draft)) in reports.iter().zip(drafts).enumerate() {
600 match resolve_report(program, layout, ri, r, draft, errors) {
601 Some(report) => writer.reports.push(report),
602 None => return Writer::default(),
603 }
604 }
605 for u in &program.report_writer.uses {
606 let found: Vec<(usize, usize)> = writer
607 .reports
608 .iter()
609 .enumerate()
610 .filter(|(_, r)| u.qualifier.as_ref().is_none_or(|q| r.name == *q))
611 .flat_map(|(ri, r)| r.groups.iter().enumerate().filter(|(_, g)| g.name.as_deref() == Some(u.group.as_str())).map(move |(gi, _)| (ri, gi)))
612 .collect();
613 match found.as_slice() {
614 [(ri, gi)] => writer.reports[*ri].groups[*gi].declarative = Some((u.section, crate::section_end(program, u.section))),
615 [] => errors.push(Error::at(u.pos, format!("USE BEFORE REPORTING {}: no report group has that name", u.group))),
616 _ => errors.push(Error::at(u.pos, format!("USE BEFORE REPORTING {}: more than one report group has that name; qualify it with IN", u.group))),
617 }
618 }
619 writer
620}
621
622fn field_content(r: &rw::Report, e: &rw::Entry, f: &DraftField, total: Option<usize>, check: &mut crate::Check) -> FieldContent {
623 match &e.content {
624 None => FieldContent::Program,
625 Some(rw::Content::Value(lit)) => FieldContent::Value(lit.clone()),
626 Some(rw::Content::Source(x)) => {
627 let x = qualified(x, &r.name);
628 check.expr(&x);
629 if (!matches!(x, Expr::Operand(_)) || e.rounded) && !matches!(f.category, Category::Numeric | Category::NumericEdited) {
630 check.errors.push(Error::at(e.pos, "an arithmetic SOURCE, or ROUNDED, needs a numeric PICTURE"));
631 }
632 FieldContent::Source(x)
633 }
634 Some(rw::Content::Sum(_)) => total.map_or(FieldContent::Program, FieldContent::Sum),
635 }
636}
637
638fn group_kind(kind: &GroupType) -> (GroupKind, Option<Option<ControlName>>) {
639 match kind {
640 GroupType::ReportHeading => (GroupKind::ReportHeading, None),
641 GroupType::PageHeading => (GroupKind::PageHeading, None),
642 GroupType::ControlHeading(c) => (GroupKind::ControlHeading, Some(c.clone())),
643 GroupType::Detail => (GroupKind::Detail, None),
644 GroupType::ControlFooting(c) => (GroupKind::ControlFooting, Some(c.clone())),
645 GroupType::PageFooting => (GroupKind::PageFooting, None),
646 GroupType::ReportFooting => (GroupKind::ReportFooting, None),
647 }
648}
649
650fn check_lines(lines: &[(LineNumber, Vec<DraftField>)], kind: GroupKind, paged: bool, pos: Pos, errors: &mut Vec<Error>) {
652 let relative = matches!(lines.first().map(|l| l.0), Some(LineNumber::Plus(_)));
653 let mut previous: Option<u32> = None;
654 for (li, (number, _)) in lines.iter().enumerate() {
655 match *number {
656 LineNumber::Line(n) | LineNumber::NextPage(Some(n)) => {
657 if !paged {
658 errors.push(Error::at(pos, "an absolute LINE needs a PAGE LIMIT"));
659 }
660 if relative {
661 errors.push(Error::at(pos, "a report group whose first LINE is relative must have only relative LINEs"));
662 }
663 if previous.is_some_and(|p| n <= p) {
664 errors.push(Error::at(pos, "absolute LINE numbers in a report group must increase"));
665 }
666 previous = Some(n);
667 }
668 LineNumber::Plus(k) => previous = previous.map(|p| p + k),
669 LineNumber::NextPage(None) => {}
670 }
671 if matches!(number, LineNumber::NextPage(_)) {
672 if !paged {
673 errors.push(Error::at(pos, "NEXT PAGE needs a PAGE LIMIT"));
674 }
675 if li > 0 {
676 errors.push(Error::at(pos, "NEXT PAGE on a LINE other than a group's first (MULTIPLE PAGE) is not supported yet"));
677 }
678 if matches!(kind, GroupKind::PageHeading | GroupKind::PageFooting) {
679 errors.push(Error::at(pos, "a PAGE HEADING or PAGE FOOTING cannot begin on the NEXT PAGE"));
680 }
681 }
682 }
683}
684
685fn resolve_report(program: &Program, layout: &Layout, ri: usize, r: &rw::Report, draft: Draft, errors: &mut Vec<Error>) -> Option<Report> {
686 let reports = &program.report_writer.reports;
687 let file = draft.file?;
688 let root = (0..layout.items.len()).rev().find(|&i| {
689 let it = &layout.items[i];
690 it.parent.is_none() && it.file.is_none() && it.linkage.is_none() && !it.local && it.name.as_deref() == Some(r.name.as_str())
691 });
692 let Some(root) = root.filter(|&i| layout.items[i].children.len() == draft.children) else {
693 errors.push(Error::at(r.pos, format!("report {}: its report control area could not be laid out", r.name)));
694 return None;
695 };
696 let children = &layout.items[root].children;
697 let child = |ordinal: usize| children[ordinal];
698 let paged = r.page.is_some();
699 let mut check = crate::Check { layout, program, errors, debugging: false, max_digits: 31, inline_performs: 0 };
700 let mut controls = Vec::new();
701 for (c, &(saved, len)) in r.controls.iter().zip(&draft.controls) {
702 check.reference(c);
703 if !c.subscripts.is_empty() || c.refmod.is_some() {
704 check.errors.push(Error::at(c.pos, format!("CONTROL {}: a subscripted or reference-modified control is not supported yet", c.name)));
705 }
706 controls.push(Control { reference: c.clone(), saved, len });
707 }
708 let levels = r.controls.len();
709 let mut control_headings = vec![None; levels + 1];
710 let mut control_footings = vec![None; levels + 1];
711 let (mut report_heading, mut page_heading, mut page_footing, mut report_footing) = (None, None, None, None);
712 let mut sums: Vec<Sum> = Vec::new();
713 let mut total_of: std::collections::HashMap<(usize, usize), usize> = std::collections::HashMap::new();
714 let mut groups: Vec<Group> = Vec::new();
715 for (gi, g) in r.groups.iter().enumerate() {
716 let (kind, control) = group_kind(&g.kind);
717 let level = match control {
718 None => 0,
719 Some(None) if levels <= 1 => levels,
720 Some(None) => {
721 check.errors.push(Error::at(g.pos, "a CONTROL HEADING or FOOTING must name its control when the report has several"));
722 0
723 }
724 Some(Some(name)) => control_level(&r.controls, &name).unwrap_or_else(|| {
725 check.errors.push(Error::at(g.pos, "a CONTROL HEADING or FOOTING names a control that is not in the report's CONTROL clause"));
726 0
727 }),
728 };
729 let slot = match kind {
730 GroupKind::ReportHeading => Some(&mut report_heading),
731 GroupKind::PageHeading => Some(&mut page_heading),
732 GroupKind::PageFooting => Some(&mut page_footing),
733 GroupKind::ReportFooting => Some(&mut report_footing),
734 GroupKind::ControlHeading => control_headings.get_mut(level),
735 GroupKind::ControlFooting => control_footings.get_mut(level),
736 GroupKind::Detail => None,
737 };
738 if let Some(slot) = slot {
739 if slot.is_some() {
740 check.errors.push(Error::at(g.pos, format!("report {} has two {kind:?} groups for the same level", r.name)));
741 }
742 *slot = Some(gi);
743 }
744 if !paged && matches!(kind, GroupKind::PageHeading | GroupKind::PageFooting) {
745 check.errors.push(Error::at(g.pos, "a PAGE HEADING or PAGE FOOTING needs a PAGE LIMIT"));
746 }
747 if let Some(ng) = g.next_group {
748 if !paged && !matches!(ng, NextGroup::Plus(_)) {
749 check.errors.push(Error::at(g.pos, "NEXT GROUP with a line or NEXT PAGE needs a PAGE LIMIT"));
750 }
751 if matches!(kind, GroupKind::PageHeading | GroupKind::ReportFooting) {
752 check.errors.push(Error::at(g.pos, "NEXT GROUP is not allowed in a PAGE HEADING or REPORT FOOTING"));
753 }
754 }
755 let dg = &draft.groups[gi];
756 check_lines(&dg.lines, kind, paged, g.pos, check.errors);
757 let mut totals = Vec::new();
758 for &(ei, ordinal) in &dg.totals {
759 total_of.insert((gi, ei), sums.len());
760 totals.push(sums.len());
761 sums.push(Sum { total: child(ordinal), reset: None });
762 }
763 let make = |f: &DraftField, column: usize, check: &mut crate::Check| {
764 let e = &g.entries[f.entry];
765 let content = field_content(r, e, f, total_of.get(&(gi, f.entry)).copied(), check);
766 Field { item: child(f.child), column, content, group_indicate: f.group_indicate, blank_when_zero: f.blank_when_zero, rounded: e.rounded, pos: e.pos }
767 };
768 let lines: Vec<Line> = dg.lines.iter().map(|(number, fields)| Line { number: *number, fields: fields.iter().map(|f| make(f, f.column, &mut check)).collect() }).collect();
769 let unprinted: Vec<Field> = dg.unprinted.iter().map(|f| make(f, 0, &mut check)).collect();
770 let indicate = lines.iter().flat_map(|l| &l.fields).any(|f| f.group_indicate).then_some(gi);
771 if indicate.is_some() && kind != GroupKind::Detail {
772 check.errors.push(Error::at(g.pos, "GROUP INDICATE outside a DETAIL group is not supported yet"));
773 }
774 groups.push(Group { name: g.name.clone(), kind, level, next_group: g.next_group, lines, unprinted, cross: Vec::new(), rolls: Vec::new(), totals, indicate, declarative: None });
775 }
776 if !groups.iter().any(|g| g.kind.is_body()) {
777 check.errors.push(Error::at(r.pos, format!("report {} has no CONTROL HEADING, DETAIL or CONTROL FOOTING group", r.name)));
778 }
779 let details: Vec<usize> = groups.iter().enumerate().filter(|(_, g)| g.kind == GroupKind::Detail).map(|(i, _)| i).collect();
780 let mut subtotals = Vec::new();
781 for (gi, g) in r.groups.iter().enumerate() {
782 for (ei, e) in g.entries.iter().enumerate() {
783 let Some(rw::Content::Sum(clauses)) = &e.content else { continue };
784 let Some(&s) = total_of.get(&(gi, ei)) else { continue };
785 for clause in clauses {
786 if let Some(reset) = &clause.reset {
787 match control_level(&r.controls, reset) {
788 Some(level) => sums[s].reset = Some(level),
789 None => check.errors.push(Error::at(e.pos, "RESET ON names a control that is not in the report's CONTROL clause")),
790 }
791 }
792 let mut upon = Vec::new();
793 for u in &clause.upon {
794 match details.iter().copied().find(|&d| groups[d].name.as_deref() == Some(u.name.as_str())) {
795 Some(d) => upon.push(d),
796 None => check.errors.push(Error::at(u.pos, format!("SUM ... UPON {}: not a DETAIL group of report {}", u.name, r.name))),
797 }
798 }
799 for operand in &clause.operands {
800 if let Some((or, og, oe)) = report_entry(reports, ri, operand) {
801 if or != ri {
802 check.errors.push(Error::at(operand.pos, "a SUM of an entry in another report is not supported yet"));
803 continue;
804 }
805 let source = &r.groups[og].entries[oe];
806 if entry_picture(source).as_deref().and_then(|p| places(p, program.environment.decimal_point_comma)).is_none() {
807 check.errors.push(Error::at(operand.pos, format!("SUM {}: the entry summed must be numeric", operand.name)));
808 continue;
809 }
810 let origin = match &source.content {
811 Some(rw::Content::Source(x)) => Origin::Source(qualified(x, &r.name)),
812 Some(rw::Content::Value(lit)) => Origin::Value(lit.clone()),
813 Some(rw::Content::Sum(_)) => match total_of.get(&(og, oe)) {
814 Some(&t) => Origin::Total(t),
815 None => continue,
816 },
817 None => {
818 check.errors.push(Error::at(operand.pos, format!("SUM {}: an entry the program fills itself cannot be summed", operand.name)));
819 continue;
820 }
821 };
822 if og == gi { groups[gi].cross.push((s, origin)) } else { groups[og].rolls.push((s, origin)) }
823 continue;
824 }
825 check.reference(operand);
826 if let Some(i) = check.item(operand)
827 && !layout.items[i].kind.is_numeric()
828 {
829 check.errors.push(Error::at(operand.pos, format!("SUM {}: not a numeric data item", operand.name)));
830 }
831 let text = ref_text(operand);
832 let correlated: Vec<usize> = details
833 .iter()
834 .copied()
835 .filter(|&d| r.groups[d].entries.iter().any(|x| matches!(&x.content, Some(rw::Content::Source(Expr::Operand(Operand::Ref(s)))) if ref_text(s) == text)))
836 .collect();
837 let adding = if !upon.is_empty() {
838 Adding::Upon(upon.clone())
839 } else if !correlated.is_empty() {
840 Adding::Correlated(correlated)
841 } else {
842 Adding::EveryGenerate
843 };
844 subtotals.push(Subtotal { sum: s, operand: Expr::Operand(Operand::Ref(operand.clone())), adding });
845 }
846 }
847 }
848 }
849 for (gi, g) in groups.iter_mut().enumerate() {
850 match order_cross(std::mem::take(&mut g.cross)) {
851 Ok(ordered) => g.cross = ordered,
852 Err(()) => check.errors.push(Error::at(r.groups[gi].pos, "SUM entries of a report group total each other in a circle")),
853 }
854 }
855 if !paged && (r.heading.is_some() || r.first_detail.is_some() || r.last_detail.is_some() || r.footing.is_some()) {
856 check.errors.push(Error::at(r.pos, "HEADING, FIRST DETAIL, LAST DETAIL and FOOTING need a PAGE LIMIT"));
857 }
858 let page = r.page.map(|limit| regions(r, limit, page_footing.map(|p| &groups[p]), check.errors));
859 Some(Report {
860 name: r.name.clone(),
861 file,
862 code: r.code.clone(),
863 width: draft.width,
864 page,
865 controls,
866 groups,
867 sums,
868 subtotals,
869 page_counter: child(0),
870 line_counter: child(1),
871 state: child(2),
872 report_heading,
873 page_heading,
874 page_footing,
875 report_footing,
876 control_headings,
877 control_footings,
878 first_detail_written: r.first_detail.map(i64::from),
879 })
880}
881
882fn order_cross(mut pending: Vec<(usize, Origin)>) -> Result<Vec<(usize, Origin)>, ()> {
884 let mut ordered = Vec::new();
885 while !pending.is_empty() {
886 let ready = |(_, origin): &(usize, Origin), pending: &[(usize, Origin)]| match origin {
887 Origin::Total(t) => !pending.iter().any(|(s, _)| s == t),
888 _ => true,
889 };
890 let Some(i) = (0..pending.len()).find(|&i| ready(&pending[i], &pending)) else { return Err(()) };
891 ordered.push(pending.remove(i));
892 }
893 Ok(ordered)
894}
895
896pub fn span(g: &Group) -> i64 {
898 g.lines.iter().map(|l| if let LineNumber::Plus(k) = l.number { k as i64 } else { 0 }).sum()
899}
900
901fn regions(r: &rw::Report, limit: u32, page_footing: Option<&Group>, errors: &mut Vec<Error>) -> Page {
904 let limit = limit as i64;
905 let heading = r.heading.map_or(1, i64::from);
906 let before_footing = match page_footing.and_then(|g| g.lines.first().map(|l| (l.number, g))) {
907 Some((LineNumber::Line(n), _)) => n as i64 - 1,
908 Some((_, g)) => limit - span(g),
909 None => limit,
910 };
911 let footing = match (r.footing, r.last_detail) {
912 (Some(Footing::Line(n)), _) => n as i64,
913 (Some(Footing::Plus(k)), Some(ld)) => ld as i64 + k as i64,
914 (Some(Footing::Plus(_)), None) => before_footing,
915 (None, Some(ld)) => ld as i64,
916 (None, None) => before_footing,
917 };
918 let last_detail = match (r.last_detail, r.footing) {
919 (Some(ld), _) => ld as i64,
920 (None, Some(Footing::Plus(k))) => footing - k as i64,
921 (None, _) => footing,
922 };
923 let first_detail = r.first_detail.map_or(heading, i64::from);
924 let limit = limit.max(last_detail).max(footing);
925 if !(1 <= heading && heading <= first_detail && first_detail <= last_detail && last_detail <= footing) {
926 errors.push(Error::at(r.pos, format!("report {}: the page regions must run HEADING <= FIRST DETAIL <= LAST DETAIL <= FOOTING <= PAGE LIMIT", r.name)));
927 }
928 Page { limit, heading, first_detail, last_detail, footing }
929}
930
931pub fn generate_target(reports: &[Report], name: &str, qualifier: Option<&str>) -> Option<(usize, Option<usize>)> {
933 if qualifier.is_none()
934 && let Some(ri) = reports.iter().position(|r| r.name == name)
935 {
936 return Some((ri, None));
937 }
938 let mut found = reports
939 .iter()
940 .enumerate()
941 .filter(|(_, r)| qualifier.is_none_or(|q| r.name == q))
942 .flat_map(|(ri, r)| r.groups.iter().enumerate().filter(|(_, g)| g.kind == GroupKind::Detail && g.name.as_deref() == Some(name)).map(move |(gi, _)| (ri, Some(gi))));
943 let first = found.next()?;
944 found.next().is_none().then_some(first)
945}
946
947pub(crate) fn check_statement(program: &Program, s: &ReportStmt, errors: &mut Vec<Error>) {
949 let reports = &program.report_writer.reports;
950 match s {
951 ReportStmt::Initiate { reports: names, pos } | ReportStmt::Terminate { reports: names, pos } => {
952 for n in names {
953 if !reports.iter().any(|r| r.name == *n) {
954 errors.push(Error::at(*pos, format!("{n} is not a report of this program")));
955 }
956 }
957 }
958 ReportStmt::Generate { name, qualifier, pos } => {
959 if qualifier.is_none()
960 && let Some(r) = reports.iter().find(|r| r.name == *name)
961 {
962 if !r.groups.iter().any(|g| matches!(g.kind, GroupType::ControlHeading(_) | GroupType::ControlFooting(_))) {
963 errors.push(Error::at(*pos, format!("GENERATE {name}: summary reporting needs a CONTROL HEADING or CONTROL FOOTING group")));
964 }
965 return;
966 }
967 let found = reports
968 .iter()
969 .filter(|r| qualifier.as_ref().is_none_or(|q| r.name == *q))
970 .flat_map(|r| &r.groups)
971 .filter(|g| g.name.as_deref() == Some(name.as_str()))
972 .collect::<Vec<_>>();
973 match found.as_slice() {
974 [g] if g.kind == GroupType::Detail => {}
975 [_] => errors.push(Error::at(*pos, format!("GENERATE {name}: not a DETAIL group"))),
976 [] => errors.push(Error::at(*pos, format!("GENERATE {name}: no report or DETAIL group of that name"))),
977 _ => errors.push(Error::at(*pos, format!("GENERATE {name}: more than one report has a DETAIL group of that name; qualify it with IN"))),
978 }
979 }
980 ReportStmt::Suppress { .. } => {}
981 }
982}
983