Skip to main content

ironwork_exec/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, Bound, CallArg, CallTarget, Ccsid, Chars, Comparand, Compare, Cond, Convert, ConvertTable, Count, DisplayItem, Expr, FileVerb, Flag, Func, HostPlace, IntExpr,
8    JsonValue, Marker, Markup, MethodName, MovePlan, Named, Op, Operand, ParseValue, Place, PlaceId, Program, RangeKind, Receiver, Replacement, SetTo, SqlStatement, StartKey,
9    StorePlan, SymId, Terminator, UpDown, XmlValue,
10};
11
12type Check<'a, T> = &'a dyn Fn(T) -> Result<(), String>;
13
14/// A CICS command's handles, each checked against its table.
15struct Ids<'a> {
16    place: Check<'a, PlaceId>,
17    operand: &'a dyn Fn(&Operand) -> Result<(), String>,
18    symbol: Check<'a, SymId>,
19}
20
21impl Handles<PlaceId, Operand, SymId> for Ids<'_> {
22    type Place = ();
23    type Value = ();
24    type Text = ();
25    type Error = String;
26
27    fn place(&mut self, place: PlaceId) -> Result<(), String> {
28        (self.place)(place)
29    }
30
31    fn value(&mut self, value: Operand) -> Result<(), String> {
32        (self.operand)(&value)
33    }
34
35    fn text(&mut self, text: SymId) -> Result<(), String> {
36        (self.symbol)(text)
37    }
38}
39
40/// A class definition's data and methods are programs of their own, each checked as one.
41pub fn verify(p: &Program) -> Result<(), String> {
42    verify_program(p)?;
43    let Some(class) = &p.services.class else { return Ok(()) };
44    for part in class.factory.iter().chain(&class.object) {
45        verify(&part.data).map_err(|e| format!("class data: {e}"))?;
46    }
47    for m in &class.methods {
48        verify(&m.code).map_err(|e| format!("method {}: {e}", p.symbols.get(m.name as usize).map_or("", String::as_str)))?;
49    }
50    let symbol = |id: u32| if (id as usize) < p.symbols.len() { Ok(()) } else { Err(format!("symbol {id} of {}", p.symbols.len())) };
51    symbol(class.external)?;
52    symbol(class.parent)?;
53    for m in &class.methods {
54        symbol(m.name)?;
55        m.params.iter().chain(&m.returns).try_for_each(|&s| symbol(s))?;
56    }
57    Ok(())
58}
59
60fn verify_program(p: &Program) -> Result<(), String> {
61    let within = |what: &str, id: u32, len: usize| if (id as usize) < len { Ok(()) } else { Err(format!("{what} {id} of {len}")) };
62    let block = |id| within("block", id, p.blocks.len());
63    let place = |id| within("place", id, p.places.len());
64    let expr = |id| within("expression", id, p.exprs.len());
65    let cond = |id| within("condition", id, p.conds.len());
66    let abend = |id| within("abend", id, p.abends.len());
67    let range = |id: u32, kind: RangeKind| match p.ranges.get(id as usize) {
68        Some(r) if r.kind == kind => Ok(()),
69        Some(r) => Err(format!("range {id} is {:?} where {kind:?} is wanted", r.kind)),
70        None => Err(format!("range {id} of {}", p.ranges.len())),
71    };
72    let places = |qs: &[u32]| qs.iter().try_for_each(|&q| place(q));
73    let int = |e: &IntExpr| match e {
74        IntExpr::Const(_) | IntExpr::Walk(_) => Ok(()),
75        IntExpr::Item(q) => place(*q),
76        IntExpr::Fixed { expr: e, prepass, .. } => expr(*e).and_then(|()| places(prepass)),
77    };
78    let operand = |o: &Operand| match *o {
79        Operand::Load(q) | Operand::LengthOf(q) | Operand::AddressOf(q) => place(q),
80        Operand::Const(c) => within("constant", c, p.consts.len()),
81        Operand::Function(f) => within("function plan", f, p.plans.function.len()),
82    };
83    let comparand = |c: &Comparand| match c {
84        Comparand::Operand(o) => operand(o),
85        Comparand::Expr { expr: e, prepass, .. } => expr(*e).and_then(|()| places(prepass)),
86    };
87    let ssrange = p.options.ssrange;
88
89    for (k, q) in p.places.iter().enumerate() {
90        let Place { subscripts, odo, refmod, at, .. } = q;
91        within("debug entry", *at, p.debug.positions.len())?;
92        for s in subscripts {
93            int(&s.value)?;
94            if s.check.is_some() != ssrange {
95                return Err(format!("place {k}: a subscript check without SSRANGE, or none with it"));
96            }
97        }
98        if let Some(o) = odo {
99            int(&o.object)?;
100            if o.check != ssrange {
101                return Err(format!("place {k}: an OCCURS DEPENDING ON check that disagrees with SSRANGE"));
102            }
103        }
104        if let Some(r) = refmod {
105            int(&r.start)?;
106            r.length.as_ref().map_or(Ok(()), int)?;
107            if r.check != ssrange {
108                return Err(format!("place {k}: a reference-modification check that disagrees with SSRANGE"));
109            }
110        }
111    }
112    for e in &p.exprs {
113        match e {
114            Expr::Operand(o) => operand(o)?,
115            Expr::Neg(a) => expr(*a)?,
116            Expr::Bin(a, _, b) => {
117                expr(*a)?;
118                expr(*b)?;
119            }
120            Expr::Pow(a, n) => {
121                expr(*a)?;
122                int(n)?;
123            }
124        }
125    }
126    for c in &p.conds {
127        match c {
128            Cond::Rel { a, b, .. } => {
129                comparand(a)?;
130                comparand(b)?;
131            }
132            Cond::Class { place: q, .. } => place(*q)?,
133            Cond::Sign { value, .. } => comparand(value)?,
134            Cond::Name { subject, values, .. } => {
135                place(*subject)?;
136                for &(low, high) in values {
137                    within("constant", low, p.consts.len())?;
138                    high.map_or(Ok(()), |h| within("constant", h, p.consts.len()))?;
139                }
140            }
141            Cond::Not(a) => cond(*a)?,
142            Cond::And(a, b) | Cond::Or(a, b) => {
143                cond(*a)?;
144                cond(*b)?;
145            }
146            Cond::Counter(_) | Cond::Sql(_) => {}
147            Cond::InTable { index, count } => {
148                place(*index)?;
149                if let Count::Odo(o) = count {
150                    int(&o.object)?;
151                }
152            }
153        }
154    }
155    for a in &p.abends {
156        a.at.map_or(Ok(()), |at| within("debug entry", at, p.debug.positions.len()))?;
157    }
158    for a in &p.plans.arith {
159        places(&a.prepass)?;
160        for s in &a.steps {
161            place(s.target)?;
162            expr(s.expr)?;
163            places(&s.probe)?;
164        }
165        if let Some(r) = &a.remainder {
166            place(r.target)?;
167            expr(r.dividend)?;
168            expr(r.divisor)?;
169        }
170    }
171    let symbol = |id: u32| within("symbol", id, p.symbols.len());
172    if p.plans.function.iter().any(|f| f.func == Func::WhenCompiled) != p.options.when_compiled.is_some() {
173        return Err("a compile time without a WHEN-COMPILED plan, or a WHEN-COMPILED plan without one".into());
174    }
175    for f in &p.plans.function {
176        for a in &f.args {
177            match a {
178                Argument::Value(c) => comparand(c)?,
179                Argument::All { element, all } => {
180                    place(*element)?;
181                    let subscripts = p.places.get(*element as usize).map_or(0, |q| q.subscripts.len());
182                    for (position, count) in all {
183                        if *position as usize >= subscripts {
184                            return Err(format!("an ALL subscript at {position} of place {element}, which has {subscripts}"));
185                        }
186                        if let Count::Odo(o) = count {
187                            int(&o.object)?;
188                            if o.check != ssrange {
189                                return Err("an ALL subscript's OCCURS DEPENDING ON check that disagrees with SSRANGE".into());
190                            }
191                        }
192                    }
193                }
194            }
195        }
196        f.integer.as_ref().map_or(Ok(()), int)?;
197        if let Some(r) = &f.refmod {
198            int(&r.start)?;
199            r.length.as_ref().map_or(Ok(()), int)?;
200            if r.check {
201                return Err("a FUNCTION's reference modification with an SSRANGE check".into());
202            }
203        }
204        f.arity.map_or(Ok(()), abend)?;
205        within("debug entry", f.at, p.debug.positions.len())?;
206    }
207    for f in &p.services.files {
208        symbol(f.name)?;
209        symbol(f.assign)?;
210        f.error.map_or(Ok(()), |r| range(r, RangeKind::UseProcedure))?;
211        f.status.map_or(Ok(()), |(q, _)| place(q))?;
212        if let Some(r) = &f.relative {
213            place(r.place)?;
214            int(&r.value)?;
215        }
216        if let Some(l) = &f.linage {
217            int(&l.lines)?;
218            [&l.footing, &l.top, &l.bottom].into_iter().flatten().try_for_each(int)?;
219            l.counter.map_or(Ok(()), |(q, _)| place(q))?;
220        }
221    }
222    let declaratives = &p.services.declaratives;
223    declaratives.modes.iter().flatten().try_for_each(|&r| range(r, RangeKind::UseProcedure))?;
224    if let Some((offset, len)) = declaratives.debug_item
225        && offset.checked_add(len).is_none_or(|end| end > p.storage.size)
226    {
227        return Err(format!("DEBUG-ITEM at {offset} for {len} in a slab of {}", p.storage.size));
228    }
229    for op in &p.services.file_ops {
230        let Some(f) = p.services.files.get(op.file as usize) else { return Err(format!("file {} of {}", op.file, p.services.files.len())) };
231        let keys = f.keys.as_ref().map_or(0, |k| k.alternates.len() + 1);
232        match &op.verb {
233            FileVerb::Open(_) | FileVerb::Close | FileVerb::CloseWith(_) | FileVerb::Delete => {}
234            FileVerb::Read { into, key, .. } => {
235                into.map_or(Ok(()), |(q, _)| place(q))?;
236                if *key != 0 && usize::from(*key) >= keys {
237                    return Err(format!("key {key} of a file with {keys}"));
238                }
239            }
240            FileVerb::Write { record, from, advancing } => {
241                place(*record)?;
242                from.map_or(Ok(()), |m| operand(&m.from).and_then(|()| place(m.to)))?;
243                if let Some(Advance::Lines { count, .. }) = advancing {
244                    int(count)?;
245                }
246            }
247            FileVerb::Rewrite { record, from } => {
248                place(*record)?;
249                from.map_or(Ok(()), |m| operand(&m.from).and_then(|()| place(m.to)))?;
250            }
251            FileVerb::Start { key, .. } => match key {
252                StartKey::Named { key, .. } if usize::from(*key) >= keys => return Err(format!("key {key} of a file with {keys}")),
253                StartKey::Relative(n) => int(n)?,
254                _ => {}
255            },
256        }
257    }
258    let chars = |c: &Chars| match c {
259        Chars::Literal(_) => Ok(()),
260        Chars::Place(q) => place(*q),
261        Chars::Value(o) => operand(o),
262    };
263    let bounds = |bs: &[Bound]| bs.iter().try_for_each(|b| chars(&b.value));
264    let store = |s: &StorePlan| if let StorePlan::Refused(a) = s { abend(*a) } else { Ok(()) };
265    let moved = |m: &MovePlan| if let MovePlan::Refused(a) = m { abend(*a) } else { Ok(()) };
266    for plan in &p.plans.string {
267        place(plan.into)?;
268        plan.pointer.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
269        for source in &plan.sources {
270            chars(&source.chars)?;
271            source.delimiter.as_ref().map_or(Ok(()), chars)?;
272        }
273    }
274    for plan in &p.plans.unstring {
275        place(plan.source)?;
276        plan.pointer.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
277        plan.delimiters.iter().try_for_each(|(_, d)| chars(d))?;
278        for field in &plan.into {
279            place(field.target)?;
280            moved(&field.plan)?;
281            if let Some(d) = &field.delimiter {
282                place(d.target)?;
283                moved(&d.found)?;
284                moved(&d.none)?;
285            }
286            field.count.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(s)))?;
287        }
288        plan.tallying.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(&s.store)))?;
289    }
290    for plan in &p.plans.inspect {
291        place(plan.target)?;
292        for phrase in plan.tallying.iter().chain(&plan.replacing) {
293            phrase.pattern.as_ref().map_or(Ok(()), chars)?;
294            if let Some(Replacement::Chars(c)) = &phrase.by {
295                chars(c)?;
296            }
297            phrase.counter.as_ref().map_or(Ok(()), |(q, s)| place(*q).and_then(|()| store(&s.store)))?;
298            bounds(&phrase.bounds)?;
299        }
300        if let Some(c) = &plan.converting {
301            if let ConvertTable::Operands { from, to } = &c.table {
302                chars(from)?;
303                chars(to)?;
304            }
305            bounds(&c.bounds)?;
306        }
307    }
308    for plan in &p.plans.search_all {
309        place(plan.index)?;
310        store(&plan.store)?;
311        if let Count::Odo(o) = &plan.count {
312            int(&o.object)?;
313        }
314        for key in &plan.keys {
315            comparand(&key.key)?;
316            comparand(&key.value)?;
317            if let Compare::Refused(a) = key.how {
318                abend(a)?;
319            }
320        }
321    }
322    for c in &p.services.calls {
323        match &c.target {
324            CallTarget::Named { name, .. } => symbol(*name)?,
325            CallTarget::Dynamic(o) => operand(o)?,
326            CallTarget::Pointer(q) => place(*q)?,
327        }
328        for a in &c.args {
329            match a {
330                CallArg::Reference(q) => place(*q)?,
331                CallArg::Content(ch) => chars(ch)?,
332                CallArg::Value(o) => operand(o)?,
333                CallArg::Omitted => {}
334            }
335            if matches!(c.target, CallTarget::Pointer(_)) && !matches!(a, CallArg::Value(_) | CallArg::Omitted) {
336                return Err("a CALL through a pointer with an argument that is not a value".into());
337            }
338        }
339        c.returning.map_or(Ok(()), place)?;
340    }
341    for i in &p.services.invokes {
342        match i.receiver {
343            Receiver::SelfRef | Receiver::Super => {}
344            Receiver::Class { name, external } => {
345                symbol(name)?;
346                symbol(external)?;
347            }
348            Receiver::Object(q) => place(q)?,
349        }
350        match i.method {
351            MethodName::New => {}
352            MethodName::Named(s) => symbol(s)?,
353            MethodName::Dynamic(q) => place(q)?,
354        }
355        for (o, java) in &i.args {
356            operand(o)?;
357            symbol(*java)?;
358        }
359        if let Some((q, java)) = i.returning {
360            place(q)?;
361            symbol(java)?;
362        }
363    }
364    for c in &p.services.cics {
365        c.clone().map(&mut Ids { place: &place, operand: &operand, symbol: &symbol })?;
366        c.command.labels().into_iter().try_for_each(|q| within("paragraph", q, p.paragraphs.len()))?;
367    }
368    let host = |hs: &[HostPlace]| {
369        hs.iter().try_for_each(|h| {
370            place(h.var)?;
371            h.ty.as_ref().map_or_else(|&a| abend(a), |_| Ok(()))?;
372            h.indicator.map_or(Ok(()), |(q, _)| place(q))
373        })
374    };
375    for e in &p.sql {
376        symbol(e.verb)?;
377        symbol(e.text)?;
378        match &e.statement {
379            SqlStatement::Query { inputs, into } => host(inputs).and_then(|()| host(into))?,
380            SqlStatement::Change { inputs, current_of, .. } => host(inputs).and_then(|()| current_of.map_or(Ok(()), symbol))?,
381            SqlStatement::Open { cursor, inputs } => symbol(*cursor).and_then(|()| host(inputs))?,
382            SqlStatement::Fetch { cursor, into } => symbol(*cursor).and_then(|()| host(into))?,
383            SqlStatement::Close { cursor: s } | SqlStatement::Unsupported(s) => symbol(*s)?,
384            SqlStatement::Commit | SqlStatement::Rollback | SqlStatement::Declaration => {}
385        }
386    }
387    p.services.sqlca.fields.iter().try_for_each(|&(_, q, _)| place(q))?;
388    let count = |c: &Count| match c {
389        Count::Fixed(_) => Ok(()),
390        Count::Odo(o) => int(&o.object),
391    };
392    let marker = |m: &Marker| match *m {
393        Marker::Byte(_) => Ok(()),
394        Marker::Condition(c) => cond(c),
395        Marker::Refused(a) => abend(a),
396    };
397    let convert = |c: &Convert| if let Convert::Refused(a) = *c { abend(a) } else { Ok(()) };
398    let constant = |c: u32| within("constant", c, p.consts.len());
399    let set_to = |s: &SetTo| match *s {
400        SetTo::Nothing => Ok(()),
401        SetTo::Move { place: q, value, .. } => place(q).and_then(|()| constant(value)),
402        SetTo::Refused(a) => abend(a),
403    };
404    let flag = |f: &Flag| match f {
405        Flag::Set { on, off } => set_to(on).and_then(|()| set_to(off)),
406        Flag::Literals { on, off } => constant(on.0).and_then(|()| constant(off.0)),
407    };
408    let ccsid = |c: &Ccsid| if let Ccsid::Operand(o) = c { operand(o) } else { Ok(()) };
409    let members = |k: usize, members: &[u32], nodes: usize| match members.iter().find(|&&m| m as usize <= k || m as usize >= nodes) {
410        Some(m) => Err(format!("markup node {k} holds node {m} of {nodes}")),
411        None => Ok(()),
412    };
413    for m in &p.services.markup {
414        match m {
415            Markup::JsonGenerate(g) => {
416                place(g.from)?;
417                g.subscripts.iter().try_for_each(int)?;
418                g.name.map_or(Ok(()), symbol)?;
419                place(g.receiver)?;
420                ccsid(&g.encoding)?;
421                g.count.map_or(Ok(()), |(q, _)| place(q))?;
422                place(g.code.0)?;
423                for (k, n) in g.nodes.iter().enumerate() {
424                    symbol(n.name)?;
425                    n.occurs.as_ref().map_or(Ok(()), count)?;
426                    match &n.value {
427                        JsonValue::Object { members: held, .. } => members(k, held, g.nodes.len())?,
428                        JsonValue::Leaf(leaf) => {
429                            if let Some((at, test)) = &leaf.indicator {
430                                at.map_or_else(abend, place)?;
431                                marker(test)?;
432                            }
433                            leaf.boolean.as_ref().map_or(Ok(()), marker)?;
434                            convert(&leaf.convert)?;
435                        }
436                    }
437                }
438            }
439            Markup::XmlGenerate(g) => {
440                place(g.receiver)?;
441                ccsid(&g.encoding)?;
442                g.namespace.iter().chain(&g.prefix).try_for_each(operand)?;
443                place(g.from)?;
444                g.subscripts.iter().try_for_each(int)?;
445                g.count.map_or(Ok(()), |(q, _)| place(q))?;
446                place(g.code.0)?;
447                for (k, n) in g.nodes.iter().enumerate() {
448                    symbol(n.name)?;
449                    n.occurs.as_ref().map_or(Ok(()), count)?;
450                    match &n.value {
451                        XmlValue::Element { members: held } | XmlValue::Members { members: held } => members(k, held, g.nodes.len())?,
452                        XmlValue::Leaf { convert: c, .. } => convert(c)?,
453                    }
454                }
455            }
456            Markup::XmlParse(x) => {
457                place(x.document)?;
458                x.encoding.as_ref().map_or(Ok(()), operand)?;
459                range(x.procedure, RangeKind::Processing)?;
460                place(x.event)?;
461                place(x.code.0)?;
462                place(x.information.0)?;
463                int(&x.code_value)?;
464            }
465            Markup::JsonParse(j) => {
466                place(j.source)?;
467                ccsid(&j.encoding)?;
468                place(j.into)?;
469                j.subscripts.iter().try_for_each(int)?;
470                place(j.code.0)?;
471                place(j.status.0)?;
472                for (k, n) in j.nodes.iter().enumerate() {
473                    if let Named::Exactly(name) | Named::Folded(name) = n.name {
474                        symbol(name)?;
475                    }
476                    n.occurs.as_ref().map_or(Ok(()), count)?;
477                    if let Some(i) = &n.indicator {
478                        i.place.map_or(Ok(()), |at| at.map_or_else(abend, place))?;
479                        flag(&i.flag)?;
480                    }
481                    match &n.value {
482                        ParseValue::Object { members: held } => members(k, held, j.nodes.len())?,
483                        ParseValue::Leaf(leaf) => leaf.boolean.as_ref().map_or(Ok(()), flag)?,
484                        ParseValue::Suppressed => {}
485                    }
486                }
487            }
488        }
489    }
490    for e in &p.services.entries {
491        symbol(e.name)?;
492        within("paragraph", e.paragraph, p.paragraphs.len())?;
493        block(e.block)?;
494        e.using.iter().try_for_each(|&r| within("LINKAGE record", u32::from(r), p.storage.linkage.len()))?;
495    }
496    for d in &p.plans.display {
497        for item in &d.items {
498            match item {
499                DisplayItem::Bytes(q) | DisplayItem::National(q) | DisplayItem::Digits { place: q, .. } => place(*q)?,
500                DisplayItem::Refused { place: q, abend: a } => {
501                    place(*q)?;
502                    abend(*a)?;
503                }
504                DisplayItem::Text(t) => within("symbol", *t, p.symbols.len())?,
505                DisplayItem::Value(o) => operand(o)?,
506            }
507        }
508    }
509
510    if p.debug.ops.len() != p.blocks.len() {
511        return Err(format!("debug entries for {} blocks of {}", p.debug.ops.len(), p.blocks.len()));
512    }
513    for (b, (blk, ids)) in p.blocks.iter().zip(&p.debug.ops).enumerate() {
514        if ids.len() != blk.ops.len() + 1 {
515            return Err(format!("block {b}: {} debug entries for {} ops and a terminator", ids.len(), blk.ops.len()));
516        }
517        ids.iter().try_for_each(|&id| within("debug entry", id, p.debug.positions.len()))?;
518        // The number of blocks the Select after an op has, 0 when the op returns no arm.
519        let arms = |op: &Op| match op {
520            Op::Arith(a) if p.plans.arith.get(*a as usize).is_some_and(|plan| plan.handled) => 2,
521            Op::Call(c) if p.services.calls.get(*c as usize).is_some_and(|plan| plan.on_exception || plan.not_on_exception) => 2,
522            Op::Invoke(i) if p.services.invokes.get(*i as usize).is_some_and(|plan| plan.on_exception || plan.not_on_exception) => 2,
523            Op::File(f) => p.services.file_ops.get(*f as usize).map_or(0, |op| op.arms()),
524            Op::String(_) | Op::Unstring(_) | Op::SearchAll(_) => 2,
525            Op::Markup(m) if p.services.markup.get(*m as usize).is_some_and(|x| x.phrases() != (false, false)) => 2,
526            _ => 0,
527        };
528        let armed = |op: &Op| arms(op) > 0;
529        let last = blk.ops.len().saturating_sub(1);
530        if blk.ops.iter().enumerate().any(|(k, op)| armed(op) && (k != last || !matches!(blk.end, Terminator::Select(_)))) {
531            return Err(format!("block {b}: an op that returns an arm is not followed by its Select"));
532        }
533        for op in &blk.ops {
534            match op {
535                Op::Move { from, to, .. } => {
536                    operand(from)?;
537                    place(*to)?;
538                }
539                Op::Initialize { target, plan } => {
540                    place(*target)?;
541                    within("INITIALIZE plan", *plan, p.plans.init.len())?;
542                }
543                Op::Arith(a) => within("arithmetic plan", *a, p.plans.arith.len())?,
544                Op::Step { var, by, prepass, .. } => {
545                    place(*var)?;
546                    expr(*by)?;
547                    places(prepass)?;
548                }
549                Op::SetTemp(_, n) => int(n)?,
550                Op::Display(d) => within("DISPLAY plan", *d, p.plans.display.len())?,
551                Op::Call(c) => within("CALL plan", *c, p.services.calls.len())?,
552                Op::Cancel(o) => operand(o)?,
553                Op::Invoke(i) => within("INVOKE plan", *i, p.services.invokes.len())?,
554                Op::Alter { para, to } => {
555                    within("paragraph", *para, p.paragraphs.len())?;
556                    within("paragraph", *to, p.paragraphs.len())?;
557                }
558                Op::File(f) => within("file statement", *f, p.services.file_ops.len())?,
559                Op::Markup(m) => within("JSON or XML statement", *m, p.services.markup.len())?,
560                Op::Cics(c) => within("EXEC CICS command", *c, p.services.cics.len())?,
561                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())),
562                Op::Sql(_) => {}
563                Op::SetAddress { records, address } => {
564                    records.iter().try_for_each(|&r| within("LINKAGE record", u32::from(r), p.storage.linkage.len()))?;
565                    operand(address)?;
566                }
567                Op::SetUpDown { by, targets, .. } => {
568                    int(by)?;
569                    for (q, how) in targets {
570                        place(*q)?;
571                        if let UpDown::Refused(a) = how {
572                            abend(*a)?;
573                        }
574                    }
575                }
576                Op::String(id) => within("STRING plan", *id, p.plans.string.len())?,
577                Op::Unstring(id) => within("UNSTRING plan", *id, p.plans.unstring.len())?,
578                Op::Inspect(id) => within("INSPECT plan", *id, p.plans.inspect.len())?,
579                Op::SearchAll(id) => within("SEARCH ALL plan", *id, p.plans.search_all.len())?,
580                Op::SetInt { target, value } => {
581                    place(*target)?;
582                    int(value)?;
583                }
584                Op::Accept { target, plan, .. } => {
585                    place(*target)?;
586                    moved(plan)?;
587                }
588                Op::DebugAlter { range: r, name, contents } => {
589                    range(*r, RangeKind::Debugging)?;
590                    symbol(*name)?;
591                    symbol(*contents)?;
592                }
593                Op::Nest | Op::Unnest(_) | Op::DecTemp(_) | Op::EnterSegment(_) | Op::DebugLine(_) => {}
594                other => return Err(format!("block {b}: {other:?} is outside this slice")),
595            }
596        }
597        match &blk.end {
598            Terminator::Jump(t) => block(*t)?,
599            Terminator::Branch { cond: c, then, otherwise } => {
600                cond(*c)?;
601                block(*then)?;
602                block(*otherwise)?;
603            }
604            Terminator::Select(targets) => {
605                targets.iter().try_for_each(|&t| block(t))?;
606                if blk.ops.last().map_or(0, arms) != targets.len() {
607                    return Err(format!("block {b}: a Select that does not follow an op with its phrases"));
608                }
609            }
610            Terminator::AlteredGoTo { para, otherwise } => {
611                within("paragraph", *para, p.paragraphs.len())?;
612                block(*otherwise)?;
613            }
614            Terminator::ParagraphEnd { next } => {
615                if *next as usize > p.paragraphs.len() {
616                    return Err(format!("block {b}: paragraph end to {next} of {}", p.paragraphs.len()));
617                }
618            }
619            Terminator::GoTo(t) => within("paragraph", *t, p.paragraphs.len())?,
620            Terminator::Switch { value, targets, otherwise } => {
621                int(value)?;
622                targets.iter().try_for_each(|&t| within("paragraph", t, p.paragraphs.len()))?;
623                block(*otherwise)?;
624            }
625            Terminator::PerformEnter { range: r, ret, resume } => {
626                range(*r, RangeKind::Perform)?;
627                block(*ret)?;
628                if let Some(resume) = resume {
629                    within("paragraph", resume.para, p.paragraphs.len())?;
630                    block(resume.block)?;
631                }
632            }
633            Terminator::Debug { range: r, name, next } => {
634                range(*r, RangeKind::Debugging)?;
635                symbol(*name)?;
636                block(*next)?;
637            }
638            Terminator::ExitProgram { next } => block(*next)?,
639            Terminator::End(_) => {}
640            Terminator::Abend(a) => abend(*a)?,
641        }
642    }
643    for (k, para) in p.paragraphs.iter().enumerate() {
644        block(para.entry)?;
645        if (para.section_end as usize) < k || para.section_end as usize >= p.paragraphs.len() {
646            return Err(format!("paragraph {k}: section end {}", para.section_end));
647        }
648        para.abandoned.map_or(Ok(()), abend)?;
649        if para.abandoned.is_some() != p.ranges.iter().any(|r| r.last as usize == k) {
650            return Err(format!("paragraph {k}: an abandoned return point's abend where no range ends, or none where one does"));
651        }
652    }
653    for r in &p.ranges {
654        within("paragraph", r.first, p.paragraphs.len())?;
655        within("paragraph", r.last, p.paragraphs.len())?;
656    }
657    if p.procedure_start as usize > p.paragraphs.len() {
658        return Err(format!("procedure start {} of {} paragraphs", p.procedure_start, p.paragraphs.len()));
659    }
660    Ok(())
661}