Skip to main content

ironwork_compile/
numcheck.rs

1//! NUMCHECK's facts known when compiled: what ZON(LAX) tolerates in each zoned item because of the
2//! item its record redefines (assumption [`numeric::assumptions::NUMCHECK_LAX_REDEFINES`]), and the
3//! tests the compiler finds always fail, which it reports with an error-level message and removes
4//! (assumption [`numeric::assumptions::NUMCHECK_ALWAYS_FAILS`]; Programming Guide SC27-8714-03,
5//! pp. 388-391).
6
7use crate::collating::Sequence;
8use crate::layout::{Kind, Layout, Resolved};
9use numeric::Options;
10use numeric::assumptions::NUMCHECK_ALWAYS_FAILS;
11use rt::picture::Sym;
12use rt::store::LaxRedefinition;
13use rt::vocab::{Figurative, SignClause, SignPosition};
14use std::collections::HashSet;
15use syntax::ast::*;
16use syntax::{Error, Pos, Severity};
17
18#[derive(Clone, Debug, Default)]
19pub struct NumcheckFacts {
20    lax: Vec<Option<LaxRedefinition>>,
21    /// Each reference, by its position, and item whose test was removed.
22    removed: HashSet<(u16, u32, u32, usize)>,
23}
24
25impl NumcheckFacts {
26    pub fn lax(&self, item: usize) -> Option<LaxRedefinition> {
27        self.lax.get(item).copied().flatten()
28    }
29
30    pub fn removed(&self, item: usize, pos: Pos) -> bool {
31        self.removed.contains(&(pos.file, pos.line, pos.col, item))
32    }
33}
34
35/// NUMCHECK's facts for `program`, with an error for each test removed.
36pub(crate) fn facts(program: &Program, layout: &Layout, declared: usize, options: &Options, collating: &Sequence, errors: &mut Vec<Error>) -> NumcheckFacts {
37    let mut facts = NumcheckFacts { lax: lax_redefinitions(layout), removed: HashSet::new() };
38    if options.numcheck.is_none() || program.oo.is_some() {
39        return facts;
40    }
41    let mut tested = Tested { layout, options, found: Vec::new(), set: Vec::new() };
42    for paragraph in &program.paragraphs {
43        tested.statements(&paragraph.statements);
44    }
45    let lengths: Vec<usize> = program.files.iter().filter_map(|f| f.record_depending.as_ref()).filter_map(|r| tested.item(r)).collect();
46    tested.set.extend(lengths);
47    let constant: HashSet<usize> = crate::initcheck::never_set(program, layout, declared, &tested.set).into_iter().collect();
48    for (pos, item, as_integer) in tested.found {
49        let key = (pos.file, pos.line, pos.col, item);
50        if !constant.contains(&item) || facts.removed.contains(&key) {
51            continue;
52        }
53        let Some(bytes) = value_bytes(layout, item, options, collating) else { continue };
54        let Some(why) = rt::store::numcheck_fault_in(options, layout.items[item].kind, &bytes, facts.lax(item), as_integer) else { continue };
55        facts.removed.insert(key);
56        let name = layout.items[item].name.as_deref().unwrap_or("FILLER");
57        let hex = rt::digest::hex(&bytes).to_ascii_uppercase();
58        errors.push(
59            syntax::messages::IWC0110.at(pos, format!("NUMCHECK: {name} {why} wherever this statement reads it: its VALUE clauses give it X'{hex}' and no statement changes it, so the test is removed (see {NUMCHECK_ALWAYS_FAILS})"))
60                .graded(Severity::Error),
61        );
62    }
63    facts
64}
65
66/// ZON(LAX)'s tolerance of each item: an unsigned zoned item whose last byte is the last of a
67/// signed trailing-overpunch item its level-01 or level-77 record redefines, or a zoned item, unsigned
68/// or signed trailing-overpunch, starting where a numeric-edited item its record redefines starts.
69/// An item in a table has none.
70fn lax_redefinitions(layout: &Layout) -> Vec<Option<LaxRedefinition>> {
71    let items = &layout.items;
72    let root = |mut i: usize| {
73        while let Some(p) = items[i].parent {
74            i = p;
75        }
76        i
77    };
78    let overpunch = |sign: Option<SignClause>| matches!(sign, None | Some(SignClause { separate: false, position: SignPosition::Trailing }));
79    (0..items.len())
80        .map(|i| {
81            let item = &items[i];
82            let Kind::Zoned { signed, sign, .. } = item.kind else { return None };
83            if !item.dims.is_empty() {
84                return None;
85            }
86            let record = root(i);
87            let target = items[record].redefines.as_deref()?;
88            let redefined = (0..record).rev().find(|&s| {
89                let s = &items[s];
90                s.parent.is_none() && matches!(s.level, 1 | 77) && s.name.as_deref() == Some(target) && s.local == items[record].local && s.linkage.is_some() == items[record].linkage.is_some()
91            })?;
92            let base = &items[redefined];
93            let from = item.offset - items[record].offset;
94            match base.kind {
95                Kind::Zoned { signed: true, sign: base_sign, .. } if !signed && overpunch(base_sign) && from + item.size == base.size => Some(LaxRedefinition::Signed),
96                Kind::NumericEdited { edit, .. } if from == 0 && (!signed || overpunch(sign)) => {
97                    let spaces = suppressed_lead(&layout.edits[edit as usize]).min(item.size);
98                    (spaces > 0).then_some(LaxRedefinition::LeadingSpaces(spaces))
99                }
100                _ => None,
101            }
102        })
103        .collect()
104}
105
106/// The positions from an edited PICTURE's start through its last leading Z, with the insertion
107/// characters among them, which zero suppression leaves as spaces.
108fn suppressed_lead(symbols: &[Sym]) -> u32 {
109    let lead = symbols.iter().take_while(|s| matches!(s, Sym::Z | Sym::Insert(_)));
110    lead.enumerate().filter(|(_, s)| **s == Sym::Z).map(|(k, _)| k as u32 + 1).last().unwrap_or(0)
111}
112
113/// The bytes the VALUE clauses give item `i` for the whole run, when one alphanumeric clause, the
114/// item's own or a group's above it, covers every byte and no other VALUE clause reaches them. An
115/// item in a table, a numeric item's own VALUE and a JUSTIFIED item's give none.
116fn value_bytes(layout: &Layout, i: usize, options: &Options, collating: &Sequence) -> Option<Vec<u8>> {
117    let items = &layout.items;
118    let item = &items[i];
119    if !item.dims.is_empty() {
120        return None;
121    }
122    let covering = std::iter::successors(Some(i), |&j| items[j].parent).find(|&j| items[j].value.is_some())?;
123    let c = &items[covering];
124    if !matches!(c.kind, Kind::Group | Kind::Alnum { justified: false }) {
125        return None;
126    }
127    let (start, end) = (item.offset, item.offset + item.size);
128    let reaches = |j: usize| {
129        let o = &items[j];
130        let span: u32 = o.dims.iter().map(|&(stride, count)| stride * count.saturating_sub(1)).sum();
131        o.local == item.local && o.offset < end && start < o.offset + o.size + span
132    };
133    if (0..items.len()).any(|j| j != covering && items[j].value.is_some() && reaches(j)) {
134        return None;
135    }
136    let image = literal_image(c.value.as_ref()?, c.size as usize, options, collating)?;
137    let from = (start - c.offset) as usize;
138    Some(image[from..from + item.size as usize].to_vec())
139}
140
141/// An alphanumeric VALUE's bytes in an item of `len` bytes.
142fn literal_image(literal: &Literal, len: usize, options: &Options, collating: &Sequence) -> Option<Vec<u8>> {
143    let characters = |literal: &Literal| match literal {
144        Literal::Alnum(s) => options.code_page().encode(s).ok(),
145        Literal::Hex(b) => Some(b.clone()),
146        Literal::Figurative(f) => Some(vec![collating.figurative(*f)]),
147        _ => None,
148    };
149    match literal {
150        Literal::Alnum(_) | Literal::Hex(_) => {
151            let mut bytes = characters(literal).filter(|b| b.len() <= len)?;
152            bytes.resize(len, collating.figurative(Figurative::Space));
153            Some(bytes)
154        }
155        Literal::Figurative(f) => Some(vec![collating.figurative(*f); len]),
156        Literal::All(inner) => {
157            let pattern = characters(inner).filter(|p| !p.is_empty())?;
158            Some(pattern.iter().copied().cycle().take(len).collect())
159        }
160        Literal::National(_) | Literal::Dbcs(_) | Literal::Number(_) => None,
161    }
162}
163
164/// The references whose items the interpreter tests where it reads them: an arithmetic expression's
165/// operands, a MOVE's sender, a numeric comparison's operands, a condition-name's conditional
166/// variable and a BY CONTENT or BY VALUE argument, each a plain reference with neither subscripts
167/// nor reference modification. The items a table SORT or a READ's record length sets, which the
168/// INITCHECK analysis does not count, are gathered too.
169struct Tested<'a> {
170    layout: &'a Layout,
171    options: &'a Options,
172    /// Each reference tested, its item, and whether it is tested as an integer.
173    found: Vec<(Pos, usize, bool)>,
174    set: Vec<usize>,
175}
176
177impl Tested<'_> {
178    fn item(&self, r: &Ref) -> Option<usize> {
179        match self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
180            Ok(Resolved::Item(i)) => Some(i),
181            _ => None,
182        }
183    }
184
185    fn plain(&self, r: &Ref) -> Option<usize> {
186        if r.refmod.is_some() || !r.subscripts.is_empty() {
187            return None;
188        }
189        self.item(r)
190    }
191
192    fn kind(&self, r: &Ref) -> Option<Kind> {
193        if r.refmod.is_some() {
194            return Some(Kind::Alnum { justified: false });
195        }
196        self.item(r).map(|i| self.layout.items[i].kind)
197    }
198
199    fn sender(&mut self, r: &Ref, as_integer: bool) {
200        if let Some(i) = self.plain(r) {
201            self.found.push((r.pos, i, as_integer));
202        }
203    }
204
205    fn statements(&mut self, stmts: &[Stmt]) {
206        stmts.iter().for_each(|s| self.statement(s));
207    }
208
209    fn opt(&mut self, stmts: &Option<Vec<Stmt>>) {
210        self.statements(stmts.as_deref().unwrap_or_default());
211    }
212
213    fn handlers(&mut self, h: &Handlers) {
214        self.opt(&h.on);
215        self.opt(&h.not_on);
216    }
217
218    fn size_error(&mut self, se: &Option<SizeError>) {
219        if let Some(se) = se {
220            self.statements(&se.on);
221            self.statements(&se.not_on);
222        }
223    }
224
225    fn statement(&mut self, s: &Stmt) {
226        match s {
227            Stmt::Move { from: Operand::Ref(r), to, .. } => self.moved(r, to),
228            Stmt::Move { .. } => {}
229            Stmt::Compute { expr, size_error, .. } => {
230                self.arithmetic(expr);
231                self.size_error(size_error);
232            }
233            Stmt::Arith(a) => {
234                a.computations.iter().for_each(|(_, e)| self.arithmetic(e));
235                if let Some((_, x, y)) = &a.remainder {
236                    self.arithmetic(x);
237                    self.arithmetic(y);
238                }
239                self.size_error(&a.size_error);
240            }
241            Stmt::If { cond, then, otherwise, .. } => {
242                self.cond(cond);
243                self.statements(then);
244                self.statements(otherwise);
245            }
246            Stmt::Evaluate { subjects, whens, other, .. } => {
247                for subject in subjects {
248                    if let Subject::Cond(c) = subject {
249                        self.cond(c);
250                    }
251                }
252                for object in whens.iter().flat_map(|w| &w.alternatives).flatten() {
253                    if let Object::Cond(c) = object {
254                        self.cond(c);
255                    }
256                }
257                whens.iter().for_each(|w| self.statements(&w.body));
258                self.statements(other);
259            }
260            Stmt::PerformInline { body, repeat, .. } => {
261                self.repeat(repeat);
262                self.statements(body);
263            }
264            Stmt::PerformProc { repeat, .. } => self.repeat(repeat),
265            Stmt::Read(r) => {
266                self.handlers(&r.at_end);
267                self.handlers(&r.invalid);
268            }
269            Stmt::Write { invalid, end_of_page, .. } => {
270                self.handlers(invalid);
271                self.handlers(end_of_page);
272            }
273            Stmt::Rewrite { invalid, .. } | Stmt::Delete { invalid, .. } | Stmt::Start { invalid, .. } => self.handlers(invalid),
274            Stmt::Call(c) => {
275                for arg in &c.using {
276                    if let (ArgMode::Content | ArgMode::Value, Some(Operand::Ref(r))) = (arg.mode, &arg.value) {
277                        self.sender(r, false);
278                    }
279                }
280                self.opt(&c.on_exception);
281                self.opt(&c.not_on_exception);
282            }
283            Stmt::Invoke(i) => {
284                self.opt(&i.on_exception);
285                self.opt(&i.not_on_exception);
286            }
287            Stmt::String(st) => {
288                self.opt(&st.on_overflow);
289                self.opt(&st.not_on_overflow);
290            }
291            Stmt::Unstring(u) => {
292                self.opt(&u.on_overflow);
293                self.opt(&u.not_on_overflow);
294            }
295            Stmt::Search(se) => {
296                self.opt(&se.at_end);
297                se.whens.iter().for_each(|(_, body)| self.statements(body));
298            }
299            Stmt::XmlParse(x) => {
300                self.opt(&x.on_exception);
301                self.opt(&x.not_on_exception);
302            }
303            Stmt::XmlGenerate(x) => {
304                self.opt(&x.on_exception);
305                self.opt(&x.not_on_exception);
306            }
307            Stmt::JsonParse(j) => {
308                self.opt(&j.on_exception);
309                self.opt(&j.not_on_exception);
310            }
311            Stmt::JsonGenerate(g) => {
312                self.opt(&g.on_exception);
313                self.opt(&g.not_on_exception);
314            }
315            Stmt::Sorting(so) => match &**so {
316                Sorting::Sort(sort) => {
317                    let table = self.item(&sort.subject);
318                    self.set.extend(table);
319                }
320                Sorting::Return { at_end, .. } => self.handlers(at_end),
321                Sorting::Release { .. } => {}
322            },
323            Stmt::Display { .. }
324            | Stmt::Open { .. }
325            | Stmt::Close { .. }
326            | Stmt::DeleteFile { .. }
327            | Stmt::Initialize { .. }
328            | Stmt::GoTo { .. }
329            | Stmt::GoToDepending { .. }
330            | Stmt::Alter { .. }
331            | Stmt::Entry { .. }
332            | Stmt::Goback { .. }
333            | Stmt::ExitProgram { .. }
334            | Stmt::ExitMethod { .. }
335            | Stmt::StopRun { .. }
336            | Stmt::Cancel { .. }
337            | Stmt::Set { .. }
338            | Stmt::Accept { .. }
339            | Stmt::Inspect(_)
340            | Stmt::NextSentence
341            | Stmt::SentenceEnd
342            | Stmt::Exec(_)
343            | Stmt::Report(_)
344            | Stmt::Continue { .. }
345            | Stmt::Exit { .. }
346            | Stmt::Corresponding(_) => {}
347        }
348    }
349
350    /// A MOVE's sender is tested for each receiver as the interpreter's `move_source` tests it: an
351    /// alphanumeric sender as an integer for a numeric receiver, and under ZON(LAX) a zoned sender
352    /// not for a zoned, alphanumeric or group receiver.
353    fn moved(&mut self, from: &Ref, to: &[Ref]) {
354        let Some(sender) = self.plain(from).map(|i| self.layout.items[i].kind) else { return };
355        let lax = self.options.numcheck.and_then(|c| c.zon).is_some_and(|z| z.lax);
356        for receiver in to {
357            let Some(kind) = self.kind(receiver) else { continue };
358            let numeric = matches!(kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_) | Kind::NumericEdited { .. });
359            let spared = lax && matches!(sender, Kind::Zoned { .. }) && matches!(kind, Kind::Zoned { .. } | Kind::Alnum { .. } | Kind::Group);
360            if !spared {
361                self.sender(from, numeric && matches!(sender, Kind::Alnum { .. } | Kind::Group));
362            }
363        }
364    }
365
366    fn repeat(&mut self, repeat: &Loop) {
367        match repeat {
368            Loop::Once | Loop::Forever => {}
369            Loop::Times(e) => self.arithmetic(e),
370            Loop::Until { cond, .. } => self.cond(cond),
371            Loop::Varying { varying, after, .. } => {
372                for v in std::iter::once(&**varying).chain(after) {
373                    self.cond(&v.until);
374                }
375            }
376        }
377    }
378
379    fn arithmetic(&mut self, e: &Expr) {
380        match e {
381            Expr::Operand(Operand::Ref(r)) => self.sender(r, false),
382            Expr::Operand(_) => {}
383            Expr::Neg(inner) => self.arithmetic(inner),
384            Expr::Bin(a, _, b) => {
385                self.arithmetic(a);
386                self.arithmetic(b);
387            }
388        }
389    }
390
391    fn cond(&mut self, c: &Cond) {
392        match c {
393            Cond::Rel(a, _, b) => {
394                self.compared(a, b);
395                self.compared(b, a);
396            }
397            Cond::Name(r) => {
398                if r.refmod.is_none()
399                    && r.subscripts.is_empty()
400                    && let Ok(Resolved::Condition(k)) = self.layout.resolve(&r.name, &r.qualifiers, r.pos)
401                {
402                    self.found.push((r.pos, self.layout.conditions[k].item, false));
403                }
404            }
405            Cond::Not(inner) => self.cond(inner),
406            Cond::And(a, b) | Cond::Or(a, b) => {
407                self.cond(a);
408                self.cond(b);
409            }
410            Cond::Class(..) | Cond::NameOrRel { .. } => {}
411        }
412    }
413
414    /// One side of a relation: an arithmetic expression's operands are tested, and a numeric item
415    /// compared with a number, ZERO, an arithmetic expression or another numeric item. Where zones
416    /// are compared, as some zoned items then are by their bytes, none is.
417    fn compared(&mut self, e: &Expr, other: &Expr) {
418        if self.options.zones_compared() {
419            return;
420        }
421        let numeric = |kind: Kind| matches!(kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_));
422        match e {
423            Expr::Bin(..) | Expr::Neg(_) => self.arithmetic(e),
424            Expr::Operand(Operand::Ref(r)) => {
425                let against_number = match other {
426                    Expr::Operand(Operand::Literal(Literal::Number(_) | Literal::Figurative(Figurative::Zero))) | Expr::Bin(..) | Expr::Neg(_) => true,
427                    Expr::Operand(Operand::Ref(o)) => self.kind(o).is_some_and(numeric),
428                    Expr::Operand(_) => false,
429                };
430                if against_number && self.kind(r).is_some_and(numeric) {
431                    self.sender(r, false);
432                }
433            }
434            Expr::Operand(_) => {}
435        }
436    }
437}