Skip to main content

ironwork_exec/
report.rs

1//! The report writer at compile time. Enterprise COBOL runs Report Writer only as the Report
2//! Writer Precompiler's generated COBOL ([`numeric::assumptions::REPORT_WRITER_PRECOMPILER`]),
3//! and this follows it: each report's lines and fields are placed; its report control area
4//! (PAGE-COUNTER, LINE-COUNTER, the writer's own state and saved controls), its printed fields and
5//! its SUM totals become WORKING-STORAGE ([`numeric::assumptions::REPORT_CONTROL_AREA`]); and every
6//! name is resolved. `machine::report` runs the result.
7
8use 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
14/// Offsets in a report's state item. The item starts as X'00', so every flag starts false.
15pub mod state {
16    pub const INITIATED: usize = 0;
17    pub const GENERATED: usize = 1;
18    /// A page has begun: output has gone to the file since INITIATE.
19    pub const STARTED: usize = 2;
20    /// The PAGE HEADING is still to come on the current page, below a REPORT HEADING.
21    pub const HEADING_DUE: usize = 3;
22    pub const BODY_ON_PAGE: usize = 4;
23    /// The current page holds the REPORT HEADING alone.
24    pub const HEADING_ONLY: usize = 5;
25    /// The line the file is at, 0 before the page's first line: a fullword.
26    pub const VERTICAL: usize = 8;
27    /// NEXT GROUP's absolute line, held until the next page: a fullword.
28    pub const SAVED_NEXT_GROUP: usize = 12;
29    /// One GROUP INDICATE flag per report group, then each control's value at the last GENERATE.
30    pub const FLAGS: usize = 16;
31}
32
33/// The reports of one program.
34#[derive(Clone, Debug, Default)]
35pub struct Writer {
36    pub reports: Vec<Report>,
37    /// PRINT-SWITCH, which SUPPRESS PRINTING sets.
38    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    /// Bytes of a line: the record less the CODE, and under NOADV the control character.
47    pub width: usize,
48    pub page: Option<Page>,
49    /// Level 1 is the most major control; level 0 is FINAL.
50    pub controls: Vec<Control>,
51    pub groups: Vec<Group>,
52    pub sums: Vec<Sum>,
53    /// SUM operands outside the REPORT SECTION, added by GENERATE.
54    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    /// The CONTROL HEADING and CONTROL FOOTING of each level, FINAL first.
63    pub control_headings: Vec<Option<usize>>,
64    pub control_footings: Vec<Option<usize>>,
65    /// FIRST DETAIL when written, so a PAGE HEADING below a REPORT HEADING can be seen not to fit.
66    pub first_detail_written: Option<i64>,
67}
68
69/// The page regions, with the precompiler's defaults applied.
70#[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    /// Where its value at the last GENERATE is kept in the state item.
83    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    /// The control level of a CONTROL HEADING or FOOTING.
109    pub level: usize,
110    pub next_group: Option<NextGroup>,
111    pub lines: Vec<Line>,
112    /// Fields with no COLUMN, which are set but not printed.
113    pub unprinted: Vec<Field>,
114    /// Cross-footing: SUM entries of this group adding entries of this group, in dependency order.
115    pub cross: Vec<(usize, Origin)>,
116    /// Rolling forward: SUM entries elsewhere adding entries of this group.
117    pub rolls: Vec<(usize, Origin)>,
118    /// The SUM entries defined in this group, reset after it unless RESET defers them.
119    pub totals: Vec<usize>,
120    pub indicate: Option<usize>,
121    /// The USE BEFORE REPORTING section: its first and last paragraph.
122    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    /// First byte in the line.
135    pub column: usize,
136    pub content: FieldContent,
137    pub group_indicate: bool,
138    /// BLANK WHEN ZERO on an unedited numeric PICTURE, which the field applies after the MOVE.
139    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    /// No SOURCE, VALUE or SUM: the program's own statements fill the field.
150    Program,
151}
152
153#[derive(Clone, Debug)]
154pub struct Sum {
155    pub total: usize,
156    /// RESET ON: the control level whose break resets the total instead.
157    pub reset: Option<usize>,
158}
159
160/// What an entry adds to a total when its group is produced.
161#[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: only a GENERATE of one of these DETAIL groups adds.
179    Upon(Vec<usize>),
180    /// SOURCE SUM correlation: the DETAIL groups that have the operand as a SOURCE.
181    Correlated(Vec<usize>),
182}
183
184/// A report's geometry and where its storage went, between synthesis and resolution.
185pub(crate) struct Draft {
186    file: Option<usize>,
187    width: usize,
188    /// How many items the report's 01-level item holds.
189    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    /// Entry index and the child holding its SUM total.
198    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
235/// The PICTURE of an entry that has none but a VALUE: X(n) for a nonnumeric literal, S9(n) for a
236/// numeric one.
237fn 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
259/// Integer and decimal places of a numeric or numeric-edited PICTURE.
260fn 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
265/// The entry a SUM operand names, when it names a REPORT SECTION entry: report, group, entry.
266fn 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
280/// Places every report's lines and fields and adds its storage to WORKING-STORAGE; gives each
281/// report file a record length when its FD has none ([`numeric::assumptions::REPORT_RECORD_LENGTH`]).
282/// Under NOADV (`adv` false) a report record's first byte is its printer control character.
283pub(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
379/// The last column any printed field of the report reaches.
380fn 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
384/// The size of each CONTROL item, from a layout of the program as written.
385fn 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
402/// Places one group's lines and fields, adding a storage child for each field and SUM total
403/// ([`numeric::assumptions::REPORT_SUM_OVERFLOW`] for a total's PICTURE).
404fn 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
510/// The 1-based first column of a field of `size` columns after one ending at `last`.
511fn 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
532/// PAGE-COUNTER and LINE-COUNTER in a report group mean the group's own report's.
533fn 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
564/// A reference as text, positions aside, for SOURCE SUM correlation.
565fn 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
588/// Resolves every name of every report against the layout, and places the page regions.
589pub(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
650/// The LINE clauses of a group must suit its type and the report.
651fn 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
882/// Orders cross-foot additions so that a total is complete before another group entry adds it.
883fn 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
896/// The rows of a relative group from the line before its first to its last.
897pub 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
901/// The page regions with the defaults the precompiler takes, as it is supplied (option OSVS):
902/// [`numeric::assumptions::REPORT_PAGE_REGION_DEFAULTS`].
903fn 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
931/// The report and DETAIL group a GENERATE names: a report alone for summary reporting.
932pub 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
947/// INITIATE, GENERATE and TERMINATE must name reports and DETAIL groups the program has.
948pub(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