Skip to main content

ironwork_compile/lower/
verify.rs

1//! The checks of lir.md ยง12.1 a lowered program must pass: every id names an entry of its table,
2//! the control-flow graph is closed, every op has its debug entry, and places carry SSRANGE checks
3//! exactly when the program has SSRANGE.
4
5use rt::cics::Handles;
6use rt::lir::{
7    Advance, Argument, Base, Binding, Bound, CallArg, CallTarget, Ccsid, Chars, Comparand, Compare, Cond, Const, Convert, ConvertTable, Count, DisplayItem, InitValue, Inspected, Expr, FileVerb, Flag, Func, HostPlace, IntExpr,
8    GlobalAt, JsonValue, Marker, Markup, MethodName, MovePlan, Named, Op, Operand, ParseValue, Place, PlaceId, Program, RangeKind, Receiver, Replacement, ReportOp, RowCount, SenderCheck, SetTo, SortIo, SortPlan,
9    SqlStatement, StartKey, StorePlan, SymId, Terminator, UpDown, UserArgument, XmlValue,
10};
11use rt::report::{FieldContent, GroupKind, Origin};
12use rt::vocab::AcceptFrom;
13
14type Check<'a, T> = &'a dyn Fn(T) -> Result<(), String>;
15
16/// A CICS command's handles, each checked against its table.
17struct Ids<'a> {
18    place: Check<'a, PlaceId>,
19    operand: &'a dyn Fn(&Operand) -> Result<(), String>,
20    symbol: Check<'a, SymId>,
21}
22
23impl Handles<PlaceId, Operand, SymId> for Ids<'_> {
24    type Place = ();
25    type Value = ();
26    type Text = ();
27    type Error = String;
28
29    fn place(&mut self, place: PlaceId) -> Result<(), String> {
30        (self.place)(place)
31    }
32
33    fn value(&mut self, value: Operand) -> Result<(), String> {
34        (self.operand)(&value)
35    }
36
37    fn text(&mut self, text: SymId) -> Result<(), String> {
38        (self.symbol)(text)
39    }
40}
41
42/// A class definition's data and methods are programs of their own, each checked as one.
43pub fn verify(p: &Program) -> Result<(), String> {
44    verify_program(p)?;
45    let Some(class) = &p.services.class else { return Ok(()) };
46    for part in class.factory.iter().chain(&class.object) {
47        verify(&part.data).map_err(|e| format!("class data: {e}"))?;
48    }
49    for m in &class.methods {
50        verify(&m.code).map_err(|e| format!("method {}: {e}", p.symbols.get(m.name as usize).map_or("", String::as_str)))?;
51    }
52    let symbol = |id: u32| if (id as usize) < p.symbols.len() { Ok(()) } else { Err(format!("symbol {id} of {}", p.symbols.len())) };
53    symbol(class.external)?;
54    symbol(class.parent)?;
55    for m in &class.methods {
56        symbol(m.name)?;
57        m.params.iter().chain(&m.returns).try_for_each(|&s| symbol(s))?;
58    }
59    Ok(())
60}
61
62fn verify_program(p: &Program) -> Result<(), String> {
63    let within = |what: &str, id: u32, len: usize| if (id as usize) < len { Ok(()) } else { Err(format!("{what} {id} of {len}")) };
64    let block = |id| within("block", id, p.blocks.len());
65    let place = |id| within("place", id, p.places.len());
66    let expr = |id| within("expression", id, p.exprs.len());
67    let cond = |id| within("condition", id, p.conds.len());
68    let abend = |id| within("abend", id, p.abends.len());
69    let range = |id: u32, kind: RangeKind| match p.ranges.get(id as usize) {
70        Some(r) if r.kind == kind => Ok(()),
71        Some(r) => Err(format!("range {id} is {:?} where {kind:?} is wanted", r.kind)),
72        None => Err(format!("range {id} of {}", p.ranges.len())),
73    };
74    let places = |qs: &[u32]| qs.iter().try_for_each(|&q| place(q));
75    let int = |e: &IntExpr| match e {
76        IntExpr::Const(_) | IntExpr::Walk(_) => Ok(()),
77        IntExpr::Item(q) => place(*q),
78        IntExpr::Fixed { expr: e, prepass, .. } => expr(*e).and_then(|()| places(prepass)),
79    };
80    let operand = |o: &Operand| match *o {
81        Operand::Load(q) | Operand::LengthOf(q) | Operand::AddressOf(q) => place(q),
82        Operand::Const(c) => within("constant", c, p.consts.len()),
83        Operand::Function(f) => within("function plan", f, p.plans.function.len()),
84        Operand::UserFunction(f) => within("user-defined function invocation", f, p.services.user_functions.len()),
85    };
86    let comparand = |c: &Comparand| match c {
87        Comparand::Operand(o) => operand(o),
88        Comparand::Expr { expr: e, prepass, .. } => expr(*e).and_then(|()| places(prepass)),
89    };
90    let ssrange = p.options.ssrange;
91    let sender = |from: &Operand, check: SenderCheck| match (from, check) {
92        (_, SenderCheck::None) => operand(from),
93        (Operand::Load(q), _) if p.options.options.numcheck.is_some() => place(*q),
94        _ => Err(format!("a NUMCHECK test {check:?} of a sender that is not a data item, or without NUMCHECK")),
95    };
96    if let Some((offset, len)) = p.storage.parmcheck
97        && (p.options.options.parmcheck.is_none() || offset.checked_add(len).is_none_or(|end| end > p.storage.size))
98    {
99        return Err(format!("a PARMCHECK buffer at {offset} for {len} in a slab of {}, or without PARMCHECK", p.storage.size));
100    }
101
102    let numcheck = p.options.options.numcheck;
103    let lax = numcheck.and_then(|c| c.zon).is_some_and(|z| z.lax);
104    for (k, q) in p.places.iter().enumerate() {
105        let Place { moved, subscripts, odo, refmod, at, .. } = q;
106        within("debug entry", *at, p.debug.positions.len())?;
107        if q.numcheck.lax.is_some() && !lax || q.numcheck.removed && numcheck.is_none() {
108            return Err(format!("place {k}: NUMCHECK facts {:?} its options do not read", q.numcheck));
109        }
110        for s in subscripts {
111            int(&s.value)?;
112        }
113        // Under SSRANGE a subscripted place carries its table's range, and without SSRANGE none.
114        if q.table.is_some() != (ssrange && !subscripts.is_empty()) {
115            return Err(format!("place {k}: a table range without SSRANGE, or none with it"));
116        }
117        for o in moved.iter().chain(odo) {
118            int(&o.object)?;
119            if o.check != ssrange {
120                return Err(format!("place {k}: an OCCURS DEPENDING ON check that disagrees with SSRANGE"));
121            }
122        }
123        if let Some(r) = refmod {
124            int(&r.start)?;
125            r.length.as_ref().map_or(Ok(()), int)?;
126            if r.check != ssrange {
127                return Err(format!("place {k}: a reference-modification check that disagrees with SSRANGE"));
128            }
129        }
130    }
131    for e in &p.exprs {
132        match e {
133            Expr::Operand(o) => operand(o)?,
134            Expr::Neg(a) => expr(*a)?,
135            Expr::Bin(a, _, b) => {
136                expr(*a)?;
137                expr(*b)?;
138            }
139            Expr::Pow(a, n) => {
140                expr(*a)?;
141                int(n)?;
142            }
143        }
144    }
145    for c in &p.conds {
146        match c {
147            Cond::Rel { a, b, .. } => {
148                comparand(a)?;
149                comparand(b)?;
150            }
151            Cond::Class { place: q, .. } => place(*q)?,
152            Cond::Sign { value, .. } => comparand(value)?,
153            Cond::Name { subject, values, .. } => {
154                place(*subject)?;
155                for &(low, high) in values {
156                    within("constant", low, p.consts.len())?;
157                    high.map_or(Ok(()), |h| within("constant", h, p.consts.len()))?;
158                }
159            }
160            Cond::Not(a) => cond(*a)?,
161            Cond::And(a, b) | Cond::Or(a, b) => {
162                cond(*a)?;
163                cond(*b)?;
164            }
165            Cond::Counter(_) | Cond::Sql(_) => {}
166            Cond::InTable { index, count } => {
167                place(*index)?;
168                if let Count::Odo(o) = count {
169                    int(&o.object)?;
170                }
171            }
172        }
173    }
174    for a in &p.abends {
175        a.at.map_or(Ok(()), |at| within("debug entry", at, p.debug.positions.len()))?;
176    }
177    p.consts.iter().try_for_each(|c| if let Const::Refused(a) = c { abend(*a) } else { Ok(()) })?;
178    for a in &p.plans.arith {
179        places(&a.prepass)?;
180        for s in &a.steps {
181            place(s.target)?;
182            expr(s.expr)?;
183            places(&s.probe)?;
184        }
185        if let Some(r) = &a.remainder {
186            place(r.target)?;
187            expr(r.dividend)?;
188            expr(r.divisor)?;
189        }
190    }
191    let symbol = |id: u32| within("symbol", id, p.symbols.len());
192    if p.plans.function.iter().any(|f| f.func == Func::WhenCompiled) != p.options.when_compiled.is_some() {
193        return Err("a compile time without a WHEN-COMPILED plan, or a WHEN-COMPILED plan without one".into());
194    }
195    for f in &p.plans.function {
196        for a in &f.args {
197            match a {
198                Argument::Value(c) => comparand(c)?,
199                Argument::All { element, all } => {
200                    place(*element)?;
201                    let subscripts = p.places.get(*element as usize).map_or(0, |q| q.subscripts.len());
202                    for (position, count) in all {
203                        if *position as usize >= subscripts {
204                            return Err(format!("an ALL subscript at {position} of place {element}, which has {subscripts}"));
205                        }
206                        if let Count::Odo(o) = count {
207                            int(&o.object)?;
208                            if o.check != ssrange {
209                                return Err("an ALL subscript's OCCURS DEPENDING ON check that disagrees with SSRANGE".into());
210                            }
211                        }
212                    }
213                }
214            }
215        }
216        f.integer.as_ref().map_or(Ok(()), int)?;
217        if let Some(r) = &f.refmod {
218            int(&r.start)?;
219            r.length.as_ref().map_or(Ok(()), int)?;
220            if r.check {
221                return Err("a FUNCTION's reference modification with an SSRANGE check".into());
222            }
223        }
224        f.arity.map_or(Ok(()), abend)?;
225        within("debug entry", f.at, p.debug.positions.len())?;
226    }
227    for f in &p.services.files {
228        symbol(f.name)?;
229        symbol(f.assign)?;
230        f.error.map_or(Ok(()), |r| range(r, RangeKind::UseProcedure))?;
231        f.status.map_or(Ok(()), |(q, _)| place(q))?;
232        f.depending.map_or(Ok(()), |d| place(d.item))?;
233        f.assign_item.map_or(Ok(()), |a| place(a.place))?;
234        if let Some(r) = &f.relative {
235            place(r.place)?;
236            int(&r.value)?;
237        }
238        if let Some(l) = &f.linage {
239            int(&l.lines)?;
240            [&l.footing, &l.top, &l.bottom].into_iter().flatten().try_for_each(int)?;
241            l.counter.map_or(Ok(()), |(q, _)| place(q))?;
242        }
243    }
244    verify_scope(p, &range)?;
245    let declaratives = &p.services.declaratives;
246    declaratives.modes.iter().flatten().try_for_each(|&r| range(r, RangeKind::UseProcedure))?;
247    if let Some((offset, len)) = declaratives.debug_item
248        && offset.checked_add(len).is_none_or(|end| end > p.storage.size)
249    {
250        return Err(format!("DEBUG-ITEM at {offset} for {len} in a slab of {}", p.storage.size));
251    }
252    for op in &p.services.file_ops {
253        let Some(f) = p.services.files.get(op.file as usize) else { return Err(format!("file {} of {}", op.file, p.services.files.len())) };
254        let keys = f.keys.as_ref().map_or(0, |k| k.alternates.len() + 1);
255        match &op.verb {
256            FileVerb::Open(_) | FileVerb::Close | FileVerb::CloseWith(_) | FileVerb::Delete | FileVerb::DeleteFile => {}
257            FileVerb::Read { into, key, .. } => {
258                into.map_or(Ok(()), |(q, _)| place(q))?;
259                if *key != 0 && usize::from(*key) >= keys {
260                    return Err(format!("key {key} of a file with {keys}"));
261                }
262            }
263            FileVerb::Write { record, from, advancing } => {
264                place(*record)?;
265                from.map_or(Ok(()), |m| sender(&m.from, m.check).and_then(|()| place(m.to)))?;
266                if let Some(Advance::Lines { count, .. }) = advancing {
267                    int(count)?;
268                }
269            }
270            FileVerb::Rewrite { record, from } => {
271                place(*record)?;
272                from.map_or(Ok(()), |m| sender(&m.from, m.check).and_then(|()| place(m.to)))?;
273            }
274            FileVerb::Start { key, .. } => match key {
275                StartKey::Named { key, .. } if usize::from(*key) >= keys => return Err(format!("key {key} of a file with {keys}")),
276                StartKey::Relative(n) => int(n)?,
277                _ => {}
278            },
279        }
280    }
281    let chars = |c: &Chars| match c {
282        Chars::Literal(_) => Ok(()),
283        Chars::Place(q) => place(*q),
284        Chars::Value(o) => operand(o),
285    };
286    let bounds = |bs: &[Bound]| bs.iter().try_for_each(|b| chars(&b.value));
287    let store = |s: &StorePlan| if let StorePlan::Refused(a) = s { abend(*a) } else { Ok(()) };
288    let moved = |m: &MovePlan| if let MovePlan::Refused(a) = m { abend(*a) } else { Ok(()) };
289    for plan in &p.plans.string {
290        place(plan.into)?;
291        plan.pointer.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
292        for source in &plan.sources {
293            chars(&source.chars)?;
294            source.delimiter.as_ref().map_or(Ok(()), chars)?;
295        }
296    }
297    for plan in &p.plans.unstring {
298        place(plan.source)?;
299        plan.pointer.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
300        plan.delimiters.iter().try_for_each(|(_, d)| chars(d))?;
301        for field in &plan.into {
302            place(field.target)?;
303            moved(&field.plan)?;
304            if let Some(d) = &field.delimiter {
305                place(d.target)?;
306                moved(&d.found)?;
307                moved(&d.none)?;
308            }
309            field.count.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
310        }
311        plan.tallying.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(&s.store)))?;
312    }
313    for plan in &p.plans.inspect {
314        match &plan.target {
315            Inspected::Item(q) => place(*q)?,
316            Inspected::Value(o) => operand(o)?,
317        }
318        for phrase in plan.tallying.iter().chain(&plan.replacing) {
319            phrase.pattern.as_ref().map_or(Ok(()), chars)?;
320            if let Some(Replacement::Chars(c)) = &phrase.by {
321                chars(c)?;
322            }
323            phrase.counter.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(&s.store)))?;
324            bounds(&phrase.bounds)?;
325        }
326        if let Some(c) = &plan.converting {
327            if let ConvertTable::Operands { from, to } = &c.table {
328                chars(from)?;
329                chars(to)?;
330            }
331            bounds(&c.bounds)?;
332        }
333    }
334    for plan in &p.plans.search_all {
335        place(plan.index)?;
336        store(&plan.store)?;
337        if let Count::Odo(o) = &plan.count {
338            int(&o.object)?;
339        }
340        for key in &plan.keys {
341            comparand(&key.key)?;
342            comparand(&key.value)?;
343            if let Compare::Refused(a) = key.how {
344                abend(a)?;
345            }
346        }
347    }
348    for c in &p.services.calls {
349        match &c.target {
350            CallTarget::Named { name, .. } => symbol(*name)?,
351            CallTarget::Dynamic(o) => operand(o)?,
352            CallTarget::Pointer(q) | CallTarget::Entry(q) => place(*q)?,
353        }
354        for a in &c.args {
355            match a {
356                CallArg::Reference(q) => place(*q)?,
357                CallArg::Content(ch) => chars(ch)?,
358                CallArg::Value(o) => operand(o)?,
359                CallArg::Omitted => {}
360            }
361            if matches!(c.target, CallTarget::Pointer(_)) && !matches!(a, CallArg::Value(_) | CallArg::Omitted) {
362                return Err("a CALL through a pointer with an argument that is not a value".into());
363            }
364        }
365        c.returning.map_or(Ok(()), place)?;
366    }
367    for i in &p.services.invokes {
368        match i.receiver {
369            Receiver::SelfRef | Receiver::Super => {}
370            Receiver::Class { name, external } => {
371                symbol(name)?;
372                symbol(external)?;
373            }
374            Receiver::Object(q) => place(q)?,
375        }
376        match i.method {
377            MethodName::New => {}
378            MethodName::Named(s) => symbol(s)?,
379            MethodName::Dynamic(q) => place(q)?,
380        }
381        for (o, java) in &i.args {
382            operand(o)?;
383            symbol(*java)?;
384        }
385        if let Some((q, java)) = i.returning {
386            place(q)?;
387            symbol(java)?;
388        }
389    }
390    for u in &p.services.user_functions {
391        symbol(u.name)?;
392        symbol(u.external)?;
393        for a in &u.args {
394            match a {
395                UserArgument::Reference(q) => place(*q)?,
396                UserArgument::Value(c) => comparand(c)?,
397            }
398        }
399        if let Some(r) = &u.refmod {
400            int(&r.start)?;
401            r.length.as_ref().map_or(Ok(()), int)?;
402            if r.check {
403                return Err("a user-defined function's reference modification with an SSRANGE check".into());
404            }
405        }
406        within("debug entry", u.at, p.debug.positions.len())?;
407    }
408    if let Some(f) = &p.services.function {
409        let record = |q: PlaceId| p.places.get(q as usize).map(|q| q.base);
410        let using: Vec<_> = p.storage.using.iter().map(|&o| Some(Base::Linkage(o))).collect();
411        if f.params.iter().map(|&q| record(q)).ne(using) || record(f.returning) != p.storage.returning.map(Base::Linkage) {
412            return Err("a function definition whose places are not its USING and RETURNING records".into());
413        }
414    }
415    for c in &p.services.cics {
416        c.clone().map(&mut Ids { place: &place, operand: &operand, symbol: &symbol })?;
417        c.command.labels().into_iter().try_for_each(|q| within("paragraph", q, p.paragraphs.len()))?;
418    }
419    let host = |hs: &[HostPlace]| {
420        hs.iter().try_for_each(|h| {
421            place(h.var)?;
422            h.ty.as_ref().map_or_else(|&a| abend(a), |_| Ok(()))?;
423            h.indicator.map_or(Ok(()), |(q, _)| place(q))
424        })
425    };
426    let rows_host = |rows: &RowCount| match rows {
427        RowCount::Host(h) => host(std::slice::from_ref(h)),
428        RowCount::Implicit | RowCount::Constant(_) => Ok(()),
429    };
430    for e in &p.sql {
431        symbol(e.verb)?;
432        symbol(e.text)?;
433        match &e.statement {
434            SqlStatement::Query { inputs, into } => host(inputs).and_then(|()| host(into))?,
435            SqlStatement::Change { inputs, current_of, .. } => host(inputs).and_then(|()| current_of.map_or(Ok(()), symbol))?,
436            SqlStatement::Open { cursor, inputs } => symbol(*cursor).and_then(|()| host(inputs))?,
437            SqlStatement::Fetch { cursor, into } => symbol(*cursor).and_then(|()| host(into))?,
438            SqlStatement::Close { cursor: s } | SqlStatement::Unsupported(s) => symbol(*s)?,
439            SqlStatement::Connect { what, location } => symbol(*what).and_then(|()| host(location))?,
440            SqlStatement::Prepare { name, source: inputs } | SqlStatement::Execute { name, inputs } => symbol(*name).and_then(|()| host(inputs))?,
441            SqlStatement::ExecuteImmediate { source } => host(source)?,
442            SqlStatement::OpenPrepared { cursor, statement, inputs } => symbol(*cursor).and_then(|()| symbol(*statement)).and_then(|()| host(inputs))?,
443            SqlStatement::Describe { name, descriptor, .. } | SqlStatement::ExecuteDescriptor { name, descriptor } | SqlStatement::FetchDescriptor { cursor: name, descriptor } => symbol(*name).and_then(|()| place(*descriptor))?,
444            SqlStatement::PrepareInto { name, source, descriptor, .. } => symbol(*name).and_then(|()| host(source)).and_then(|()| place(*descriptor))?,
445            SqlStatement::OpenDescriptor { cursor, statement, descriptor } => symbol(*cursor).and_then(|()| symbol(*statement)).and_then(|()| place(*descriptor))?,
446            SqlStatement::FetchRowset { cursor, rows, into, .. } => {
447                symbol(*cursor)?;
448                rows_host(rows)?;
449                host(&into.iter().map(|a| a.place.clone()).collect::<Vec<_>>())?
450            }
451            SqlStatement::InsertRows { inputs, rows, .. } => {
452                rows_host(rows)?;
453                host(&inputs.iter().map(|a| a.place.clone()).collect::<Vec<_>>())?
454            }
455            SqlStatement::Call { procedure, args } => symbol(*procedure).and_then(|()| host(args))?,
456            SqlStatement::Commit | SqlStatement::Rollback | SqlStatement::Declaration => {}
457        }
458    }
459    p.services.sqlca.fields.iter().try_for_each(|&(_, q, _)| place(q))?;
460    let count = |c: &Count| match c {
461        Count::Fixed(_) | Count::Temp(_) => Ok(()),
462        Count::Odo(o) => int(&o.object),
463    };
464    let marker = |m: &Marker| match *m {
465        Marker::Byte(_) => Ok(()),
466        Marker::Condition(c) => cond(c),
467        Marker::Refused(a) => abend(a),
468    };
469    let convert = |c: &Convert| if let Convert::Refused(a) = *c { abend(a) } else { Ok(()) };
470    let constant = |c: u32| within("constant", c, p.consts.len());
471    let set_to = |s: &SetTo| match *s {
472        SetTo::Nothing => Ok(()),
473        SetTo::Move { place: q, value, .. } => place(q).and_then(|()| constant(value)),
474        SetTo::Refused(a) => abend(a),
475    };
476    let flag = |f: &Flag| match f {
477        Flag::Set { on, off } => set_to(on).and_then(|()| set_to(off)),
478        Flag::Literals { on, off } => constant(on.0).and_then(|()| constant(off.0)),
479    };
480    let ccsid = |c: &Ccsid| if let Ccsid::Operand(o) = c { operand(o) } else { Ok(()) };
481    let members = |k: usize, members: &[u32], nodes: usize| match members.iter().find(|&&m| m as usize <= k || m as usize >= nodes) {
482        Some(m) => Err(format!("markup node {k} holds node {m} of {nodes}")),
483        None => Ok(()),
484    };
485    for m in &p.services.markup {
486        match m {
487            Markup::JsonGenerate(g) => {
488                place(g.from)?;
489                g.subscripts.iter().try_for_each(int)?;
490                g.name.map_or(Ok(()), symbol)?;
491                place(g.receiver)?;
492                ccsid(&g.encoding)?;
493                g.count.map_or(Ok(()), |(q, _)| place(q))?;
494                place(g.code.0)?;
495                for (k, n) in g.nodes.iter().enumerate() {
496                    symbol(n.name)?;
497                    n.occurs.as_ref().map_or(Ok(()), count)?;
498                    if let Some((at, test)) = &n.indicator {
499                        at.map_or_else(abend, place)?;
500                        marker(test)?;
501                    }
502                    match &n.value {
503                        JsonValue::Object { members: held, .. } => members(k, held, g.nodes.len())?,
504                        JsonValue::Leaf(leaf) => {
505                            leaf.boolean.as_ref().map_or(Ok(()), marker)?;
506                            convert(&leaf.convert)?;
507                        }
508                    }
509                }
510            }
511            Markup::XmlGenerate(g) => {
512                place(g.receiver)?;
513                ccsid(&g.encoding)?;
514                g.namespace.iter().chain(&g.prefix).try_for_each(operand)?;
515                place(g.from)?;
516                g.subscripts.iter().try_for_each(int)?;
517                g.count.map_or(Ok(()), |(q, _)| place(q))?;
518                place(g.code.0)?;
519                for (k, n) in g.nodes.iter().enumerate() {
520                    symbol(n.name)?;
521                    n.occurs.as_ref().map_or(Ok(()), count)?;
522                    match &n.value {
523                        XmlValue::Element { members: held } | XmlValue::Members { members: held } => members(k, held, g.nodes.len())?,
524                        XmlValue::Leaf { convert: c, .. } => convert(c)?,
525                    }
526                }
527            }
528            Markup::XmlParse(x) => {
529                place(x.document)?;
530                x.encoding.as_ref().map_or(Ok(()), operand)?;
531                range(x.procedure, RangeKind::Processing)?;
532                place(x.event)?;
533                place(x.code.0)?;
534                place(x.information.0)?;
535                int(&x.code_value)?;
536            }
537            Markup::JsonParse(j) => {
538                place(j.source)?;
539                ccsid(&j.encoding)?;
540                place(j.into)?;
541                j.subscripts.iter().try_for_each(int)?;
542                place(j.code.0)?;
543                place(j.status.0)?;
544                for (k, n) in j.nodes.iter().enumerate() {
545                    if let Named::Exactly(name) | Named::Folded(name) = n.name {
546                        symbol(name)?;
547                    }
548                    n.occurs.as_ref().map_or(Ok(()), count)?;
549                    if let Some(i) = &n.indicator {
550                        i.place.map_or(Ok(()), |at| at.map_or_else(abend, place))?;
551                        flag(&i.flag)?;
552                    }
553                    match &n.value {
554                        ParseValue::Object { members: held } => members(k, held, j.nodes.len())?,
555                        ParseValue::Leaf(leaf) => leaf.boolean.as_ref().map_or(Ok(()), flag)?,
556                        ParseValue::Suppressed => {}
557                    }
558                }
559            }
560        }
561    }
562    let files = |k: u16| within("file", u32::from(k), p.services.files.len());
563    for s in &p.services.sorts {
564        match s {
565            SortPlan::File(f) => {
566                files(f.sd)?;
567                for io in f.input.iter().chain(&f.output) {
568                    match io {
569                        SortIo::Files(ks) => ks.iter().try_for_each(|&k| files(k))?,
570                        SortIo::Procedure(r) => range(*r, RangeKind::SortProcedure)?,
571                    }
572                }
573                place(f.sort_return)?;
574                place(f.sort_control)?;
575            }
576            SortPlan::Table(t) => {
577                place(t.first)?;
578                count(&t.count)?;
579                symbol(t.name)?;
580            }
581        }
582    }
583    for r in &p.services.releases {
584        place(r.record)?;
585        r.file.map_or(Ok(()), files)?;
586        r.from.as_ref().map_or(Ok(()), |m| sender(&m.from, m.check).and_then(|()| place(m.to)).and_then(|()| moved(&m.plan)))?;
587        place(r.sort_return)?;
588        symbol(r.name)?;
589    }
590    for r in &p.services.returns {
591        r.file.map_or(Ok(()), files)?;
592        r.into.as_ref().map_or(Ok(()), |(q, m)| place(*q).and_then(|()| moved(m)))?;
593        place(r.sort_return)?;
594        symbol(r.name)?;
595    }
596    let writer = &p.services.report;
597    let item = |k: usize| within("report item", u32::try_from(k).unwrap_or(u32::MAX), p.places.len());
598    writer.print_switch.map_or(Ok(()), item)?;
599    for r in &writer.reports {
600        within("report file", u32::try_from(r.file).unwrap_or(u32::MAX), p.services.files.len())?;
601        r.code.map_or(Ok(()), constant)?;
602        r.controls.iter().try_for_each(|c| place(c.reference))?;
603        [r.page_counter, r.line_counter, r.state].into_iter().try_for_each(item)?;
604        r.sums.iter().try_for_each(|s| item(s.total))?;
605        let group = |g: usize| within("report group", u32::try_from(g).unwrap_or(u32::MAX), r.groups.len());
606        let sum = |s: usize| within("SUM", u32::try_from(s).unwrap_or(u32::MAX), r.sums.len());
607        [r.report_heading, r.page_heading, r.page_footing, r.report_footing].into_iter().flatten().try_for_each(group)?;
608        r.control_headings.iter().chain(&r.control_footings).flatten().try_for_each(|&g| group(g))?;
609        let origin = |o: &Origin| match o {
610            Origin::Source(c) => comparand(c),
611            Origin::Value(v) => constant(*v),
612            Origin::Total(t) => sum(*t),
613        };
614        for st in &r.subtotals {
615            sum(st.sum)?;
616            comparand(&st.operand)?;
617        }
618        for g in &r.groups {
619            for f in g.lines.iter().flat_map(|l| &l.fields).chain(&g.unprinted) {
620                item(f.item)?;
621                match &f.content {
622                    FieldContent::Source(c) => comparand(c)?,
623                    FieldContent::Value(v) => constant(*v)?,
624                    FieldContent::Sum(s) => sum(*s)?,
625                    FieldContent::Program => {}
626                }
627            }
628            for (s, o) in g.cross.iter().chain(&g.rolls) {
629                sum(*s)?;
630                origin(o)?;
631            }
632            g.totals.iter().try_for_each(|&s| sum(s))?;
633            g.declarative.map_or(Ok(()), |d| range(d, RangeKind::UseBeforeReporting))?;
634        }
635    }
636    let report = |op: &ReportOp| {
637        let report = |ri: u32| match writer.reports.get(ri as usize) {
638            Some(r) => Ok(r),
639            None => Err(format!("report {ri} of {}", writer.reports.len())),
640        };
641        match *op {
642            ReportOp::Initiate(ri) | ReportOp::Terminate(ri) => report(ri).map(drop),
643            ReportOp::Generate { report: ri, detail } => {
644                let r = report(ri)?;
645                match detail.map(|d| r.groups.get(d as usize).map(|g| g.kind)) {
646                    None | Some(Some(GroupKind::Detail)) => Ok(()),
647                    Some(_) => Err(format!("GENERATE of report {ri}'s group {detail:?}, which is not a DETAIL group")),
648                }
649            }
650            ReportOp::Suppress => Ok(()),
651        }
652    };
653    for e in &p.services.entries {
654        symbol(e.name)?;
655        within("paragraph", e.paragraph, p.paragraphs.len())?;
656        block(e.block)?;
657        e.using.iter().try_for_each(|&r| within("LINKAGE record", u32::from(r), p.storage.linkage.len()))?;
658    }
659    for d in &p.plans.display {
660        for item in &d.items {
661            match item {
662                DisplayItem::Bytes(q) | DisplayItem::National(q) | DisplayItem::Digits { place: q, .. } => place(*q)?,
663                DisplayItem::Refused { place: q, abend: a } => {
664                    place(*q)?;
665                    abend(*a)?;
666                }
667                DisplayItem::Text(t) => within("symbol", *t, p.symbols.len())?,
668                DisplayItem::Value(o) => operand(o)?,
669            }
670        }
671    }
672    for f in p.plans.init.iter().flat_map(|i| &i.fields) {
673        match f.value {
674            InitValue::Default(_) => {}
675            InitValue::Value(c) => within("constant", c, p.consts.len())?,
676            InitValue::Replacing(o) => operand(&o)?,
677        }
678        match f.store {
679            MovePlan::Refused(a) | MovePlan::Numeric { store: StorePlan::Refused(a), .. } => abend(a)?,
680            _ => {}
681        }
682    }
683
684    if p.debug.ops.len() != p.blocks.len() {
685        return Err(format!("debug entries for {} blocks of {}", p.debug.ops.len(), p.blocks.len()));
686    }
687    if p.debug.statements.len() != p.blocks.len() {
688        return Err(format!("statement starts for {} blocks of {}", p.debug.statements.len(), p.blocks.len()));
689    }
690    for (b, (blk, starts)) in p.blocks.iter().zip(&p.debug.statements).enumerate() {
691        if starts.windows(2).any(|w| w[0].0 > w[1].0) || starts.last().is_some_and(|&(k, _)| k as usize > blk.ops.len()) {
692            return Err(format!("block {b}: statement starts out of order or past its {} ops and terminator", blk.ops.len()));
693        }
694        starts.iter().try_for_each(|&(_, id)| within("debug entry", id, p.debug.positions.len()))?;
695    }
696    for (b, (blk, ids)) in p.blocks.iter().zip(&p.debug.ops).enumerate() {
697        if ids.len() != blk.ops.len() + 1 {
698            return Err(format!("block {b}: {} debug entries for {} ops and a terminator", ids.len(), blk.ops.len()));
699        }
700        ids.iter().try_for_each(|&id| within("debug entry", id, p.debug.positions.len()))?;
701        // The number of blocks the Select after an op has, 0 when the op returns no arm.
702        let arms = |op: &Op| match op {
703            Op::Arith(a) if p.plans.arith.get(*a as usize).is_some_and(|plan| plan.handled) => 2,
704            Op::Call(c) if p.services.calls.get(*c as usize).is_some_and(|plan| plan.on_exception || plan.not_on_exception) => 2,
705            Op::Invoke(i) if p.services.invokes.get(*i as usize).is_some_and(|plan| plan.on_exception || plan.not_on_exception) => 2,
706            Op::File(f) => p.services.file_ops.get(*f as usize).map_or(0, |op| op.arms()),
707            Op::String(_) | Op::Unstring(_) | Op::SearchAll(_) | Op::Return(_) | Op::Accept { from: AcceptFrom::ArgumentValue | AcceptFrom::EnvironmentValue, .. } => 2,
708            Op::Markup(m) if p.services.markup.get(*m as usize).is_some_and(|x| x.phrases() != (false, false)) => 2,
709            Op::ScreenAccept { handled: true, .. } => 2,
710            _ => 0,
711        };
712        let armed = |op: &Op| arms(op) > 0;
713        let last = blk.ops.len().saturating_sub(1);
714        if blk.ops.iter().enumerate().any(|(k, op)| armed(op) && (k != last || !matches!(blk.end, Terminator::Select(_)))) {
715            return Err(format!("block {b}: an op that returns an arm is not followed by its Select"));
716        }
717        for op in &blk.ops {
718            match op {
719                Op::Move { from, to, check, .. } => {
720                    sender(from, *check)?;
721                    place(*to)?;
722                }
723                Op::Set { from, to, .. } => {
724                    operand(from)?;
725                    place(*to)?;
726                }
727                Op::Initialize { target, plan } => {
728                    place(*target)?;
729                    within("INITIALIZE plan", *plan, p.plans.init.len())?;
730                }
731                Op::Arith(a) => within("arithmetic plan", *a, p.plans.arith.len())?,
732                Op::Step { var, by, prepass, .. } => {
733                    place(*var)?;
734                    expr(*by)?;
735                    places(prepass)?;
736                }
737                Op::SetTemp(_, n) | Op::ArgumentNumber(n) => int(n)?,
738                Op::SetCount(_, o) if o.check != ssrange => return Err(format!("block {b}: a SEARCH count's check that disagrees with SSRANGE")),
739                Op::SetCount(_, o) => int(&o.object)?,
740                Op::Display(d) | Op::DisplayError(d) => within("DISPLAY plan", *d, p.plans.display.len())?,
741                Op::Environment { display, .. } => within("DISPLAY plan", *display, p.plans.display.len())?,
742                Op::ScreenDisplay { display, screen } => {
743                    within("DISPLAY plan", *display, p.plans.display.len())?;
744                    screen_ints(screen).try_for_each(int)?;
745                }
746                Op::ScreenAccept { inputs, .. } => {
747                    for i in inputs {
748                        place(i.target)?;
749                        place(i.field)?;
750                        position_ints(&i.at).try_for_each(int)?;
751                    }
752                }
753                Op::Call(c) => within("CALL plan", *c, p.services.calls.len())?,
754                Op::Cancel(o) => operand(o)?,
755                Op::Invoke(i) => within("INVOKE plan", *i, p.services.invokes.len())?,
756                Op::Alter { para, to } => {
757                    within("paragraph", *para, p.paragraphs.len())?;
758                    within("paragraph", *to, p.paragraphs.len())?;
759                }
760                Op::File(f) => within("file statement", *f, p.services.file_ops.len())?,
761                Op::Markup(m) => within("JSON or XML statement", *m, p.services.markup.len())?,
762                Op::Cics(c) => within("EXEC CICS command", *c, p.services.cics.len())?,
763                Op::Sort(s) => within("SORT or MERGE", *s, p.services.sorts.len())?,
764                Op::Release(r) => within("RELEASE", *r, p.services.releases.len())?,
765                Op::Return(r) => within("RETURN", *r, p.services.returns.len())?,
766                Op::Report(op) => report(op)?,
767                Op::Sql(k) if *k == 0 || *k as usize > p.sql.len() => return Err(format!("block {b}: EXEC SQL ordinal {k} of {}", p.sql.len())),
768                Op::Sql(_) => {}
769                Op::SetAddress { records, address } => {
770                    records.iter().try_for_each(|&r| within("LINKAGE record", u32::from(r), p.storage.linkage.len()))?;
771                    operand(address)?;
772                }
773                Op::SetEntry { entry, targets } => {
774                    operand(entry)?;
775                    places(targets)?;
776                }
777                Op::SetUpDown { by, targets, .. } => {
778                    int(by)?;
779                    for (q, how) in targets {
780                        place(*q)?;
781                        if let UpDown::Refused(a) = how {
782                            abend(*a)?;
783                        }
784                    }
785                }
786                Op::String(id) => within("STRING plan", *id, p.plans.string.len())?,
787                Op::Unstring(id) => within("UNSTRING plan", *id, p.plans.unstring.len())?,
788                Op::Inspect(id) => within("INSPECT plan", *id, p.plans.inspect.len())?,
789                Op::SearchAll(id) => within("SEARCH ALL plan", *id, p.plans.search_all.len())?,
790                Op::SetInt { target, value } => {
791                    place(*target)?;
792                    int(value)?;
793                }
794                Op::Accept { target, plan, .. } => {
795                    place(*target)?;
796                    moved(plan)?;
797                }
798                Op::DebugAlter { range: r, name, contents } => {
799                    range(*r, RangeKind::Debugging)?;
800                    symbol(*name)?;
801                    symbol(*contents)?;
802                }
803                Op::Nest | Op::Unnest(_) | Op::DecTemp(_) | Op::EnterSegment(_) | Op::DebugLine(_) => {}
804            }
805        }
806        match &blk.end {
807            Terminator::Jump(t) => block(*t)?,
808            Terminator::Branch { cond: c, then, otherwise } => {
809                cond(*c)?;
810                block(*then)?;
811                block(*otherwise)?;
812            }
813            Terminator::Select(targets) => {
814                targets.iter().try_for_each(|&t| block(t))?;
815                if blk.ops.last().map_or(0, arms) != targets.len() {
816                    return Err(format!("block {b}: a Select that does not follow an op with its phrases"));
817                }
818            }
819            Terminator::AlteredGoTo { para, otherwise } => {
820                within("paragraph", *para, p.paragraphs.len())?;
821                block(*otherwise)?;
822            }
823            Terminator::ParagraphEnd { next } => {
824                if *next as usize > p.paragraphs.len() {
825                    return Err(format!("block {b}: paragraph end to {next} of {}", p.paragraphs.len()));
826                }
827            }
828            Terminator::GoTo(t) => within("paragraph", *t, p.paragraphs.len())?,
829            Terminator::Switch { value, targets, otherwise } => {
830                int(value)?;
831                targets.iter().try_for_each(|&t| within("paragraph", t, p.paragraphs.len()))?;
832                block(*otherwise)?;
833            }
834            Terminator::PerformEnter { range: r, ret, resume } => {
835                range(*r, RangeKind::Perform)?;
836                block(*ret)?;
837                if let Some(resume) = resume {
838                    within("paragraph", resume.para, p.paragraphs.len())?;
839                    block(resume.block)?;
840                }
841            }
842            Terminator::Debug { range: r, name, next } => {
843                range(*r, RangeKind::Debugging)?;
844                symbol(*name)?;
845                block(*next)?;
846            }
847            Terminator::ExitProgram { next } => block(*next)?,
848            Terminator::End(_) => {}
849            Terminator::Abend(a) => abend(*a)?,
850        }
851    }
852    for (k, para) in p.paragraphs.iter().enumerate() {
853        block(para.entry)?;
854        if (para.section_end as usize) < k || para.section_end as usize >= p.paragraphs.len() {
855            return Err(format!("paragraph {k}: section end {}", para.section_end));
856        }
857        para.abandoned.map_or(Ok(()), abend)?;
858        if para.abandoned.is_some() != p.ranges.iter().any(|r| r.last as usize == k) {
859            return Err(format!("paragraph {k}: an abandoned return point's abend where no range ends, or none where one does"));
860        }
861    }
862    for r in &p.ranges {
863        within("paragraph", r.first, p.paragraphs.len())?;
864        within("paragraph", r.last, p.paragraphs.len())?;
865    }
866    if p.procedure_start as usize > p.paragraphs.len() {
867        return Err(format!("procedure start {} of {} paragraphs", p.procedure_start, p.paragraphs.len()));
868    }
869    Ok(())
870}
871
872/// Every record, file, symbol and range the EXTERNAL and GLOBAL tables name, and a GLOBAL record
873/// within its storage.
874fn verify_scope(p: &Program, range: &dyn Fn(u32, RangeKind) -> Result<(), String>) -> Result<(), String> {
875    let scope = &p.services.scope;
876    let within = |what: &str, id: usize, len: usize| if id < len { Ok(()) } else { Err(format!("{what} {id} of {len}")) };
877    let symbol = |id: SymId| within("symbol", id as usize, p.symbols.len());
878    let record = |o: u16| within("LINKAGE record", usize::from(o), p.storage.linkage.len());
879    let file = |k: u16| within("file", usize::from(k), p.services.files.len());
880    scope.containers.iter().chain(&scope.callable).chain(&scope.hidden).try_for_each(|&s| symbol(s))?;
881    let mut last = None;
882    for &(o, ref binding) in &scope.records {
883        record(o)?;
884        if last.is_some_and(|l| l >= o) {
885            return Err(format!("LINKAGE record {o} bound out of order"));
886        }
887        last = Some(o);
888        match *binding {
889            Binding::External { name, .. } => symbol(name)?,
890            Binding::ExternalFile(k) => file(k)?,
891            Binding::Global { program, name, .. } => symbol(program).and_then(|()| symbol(name))?,
892        }
893    }
894    for f in &scope.files {
895        file(f.file)?;
896        f.declared_in.map_or(Ok(()), symbol)?;
897        if !f.external && f.declared_in.is_none() {
898            return Err(format!("file {} shared as neither EXTERNAL nor GLOBAL", f.file));
899        }
900    }
901    for &(k, o) in &scope.areas {
902        file(k)?;
903        record(o)?;
904    }
905    for g in &scope.globals {
906        symbol(g.name)?;
907        match g.at {
908            GlobalAt::Program(offset) => within("GLOBAL record offset", offset as usize, p.storage.image.len())?,
909            GlobalAt::Local(offset) => within("GLOBAL LOCAL-STORAGE offset", offset as usize, p.storage.local_image.len())?,
910            GlobalAt::Linkage(o) => record(o)?,
911        }
912    }
913    for &(k, r) in &scope.global_files {
914        file(k)?;
915        range(r, RangeKind::UseProcedure)?;
916    }
917    scope.global_modes.iter().flatten().try_for_each(|&r| range(r, RangeKind::UseProcedure))
918}
919
920/// The integers a screen phrase evaluates.
921fn screen_ints(screen: &rt::lir::ScreenPlan) -> impl Iterator<Item = &IntExpr> {
922    position_ints(&screen.at)
923}
924
925/// The integers a screen position evaluates.
926fn position_ints(at: &rt::lir::ScreenPosition) -> impl Iterator<Item = &IntExpr> {
927    let (a, b, c) = match at {
928        rt::lir::ScreenPosition::Cursor => (None, None, None),
929        rt::lir::ScreenPosition::Combined(at) => (Some(at), None, None),
930        rt::lir::ScreenPosition::LineColumn { line, column } => (None, line.as_ref(), column.as_ref()),
931    };
932    a.into_iter().chain(b).chain(c)
933}