Skip to main content

ironwork_exec/
lib.rs

1//! ironwork for COBOL: WORKING-STORAGE laid out as IBM lays it out, and an interpreter that runs a
2//! program against it in EBCDIC with the numeric model of `ironwork-numeric`.
3
4pub mod cics;
5pub mod edit;
6pub mod files;
7pub mod layout;
8pub mod machine;
9pub mod picture;
10pub mod strings;
11pub mod terminal;
12pub mod tn3270;
13pub mod unit;
14
15pub use machine::{Abend, Ending};
16
17use layout::Layout;
18use numeric::Options;
19use std::io::{BufRead, Write};
20use syntax::ast::*;
21use syntax::{Error, Pos};
22
23pub struct Compiled {
24    pub program: Program,
25    pub layout: Layout,
26    pub options: Options,
27    pub ssrange: bool,
28}
29
30const FUNCTIONS: &[&str] = &[
31    "CHAR", "ORD", "NATIONAL-OF", "LENGTH", "UPPER-CASE", "LOWER-CASE", "REVERSE", "CURRENT-DATE", "NUMVAL", "NUMVAL-C", "TRIM", "MOD", "REM",
32    "INTEGER", "INTEGER-PART", "ABS", "MIN", "MAX", "INTEGER-OF-DATE", "DATE-OF-INTEGER",
33];
34
35/// Checks and lays out a parsed program. `flags` are this compiler's own, such as `-silent`.
36pub fn compile(program: Program, flags: &[String]) -> Result<Compiled, Vec<Error>> {
37    let mut errors = Vec::new();
38    let mut options = Options::default();
39    let mut ssrange = false;
40    for option in &program.options {
41        let upper = option.to_ascii_uppercase();
42        if upper.starts_with("SSRANGE") || upper == "SSR" {
43            ssrange = true;
44        } else if upper == "NOSSRANGE" || upper == "NOSSR" {
45            ssrange = false;
46        }
47        if let Err(e) = options.apply(option) {
48            errors.push(Error::at(Pos::default(), format!("CBL {option}: {e}")));
49        }
50    }
51    for flag in flags {
52        if let Err(e) = options.apply_flag(flag) {
53            errors.push(Error::at(Pos::default(), e.to_string()));
54        }
55    }
56    let files: Vec<(&[DataEntry], Option<u32>)> = program.files.iter().map(|f| (f.records.as_slice(), f.record_max)).collect();
57    let layout = match layout::build(&program.working_storage, &files, &program.linkage, &program.local_storage) {
58        Ok(l) => l,
59        Err(e) => {
60            errors.push(e);
61            return Err(errors.into_iter().map(|e| e.in_files(&program.sources)).collect());
62        }
63    };
64    for item in &layout.items {
65        if let Some(object) = &item.depending_on {
66            match layout.resolve(&object.name, &object.qualifiers, object.pos) {
67                Ok(layout::Resolved::Item(i)) if layout.items[i].kind.is_numeric() => {}
68                Ok(_) => errors.push(Error::at(object.pos, format!("OCCURS DEPENDING ON {}: not a numeric data item", object.name))),
69                Err(e) => errors.push(e),
70            }
71        }
72    }
73    for param in &program.using {
74        let is_record = layout.linkage_roots.iter().any(|&i| layout.items[i].name.as_deref() == Some(param.name.as_str()));
75        if !is_record {
76            errors.push(Error::at(Pos::default(), format!("PROCEDURE DIVISION USING {}: not an 01 or 77 item of the LINKAGE SECTION", param.name)));
77        }
78    }
79    let mut check = Check { layout: &layout, program: &program, errors: &mut errors };
80    for k in 0..program.files.len() {
81        check.file_keys(k);
82    }
83    for block in &program.exec_declarations {
84        check.exec_block(block);
85    }
86    for p in &program.paragraphs {
87        check.statements(&p.statements);
88    }
89    if errors.is_empty() {
90        Ok(Compiled { program, layout, options, ssrange })
91    } else {
92        Err(errors.into_iter().map(|e| e.in_files(&program.sources)).collect())
93    }
94}
95
96/// The last paragraph of the section that paragraph `i` is in, or `i` when there are no sections.
97pub(crate) fn section_end(program: &Program, i: usize) -> usize {
98    let paragraphs = &program.paragraphs;
99    let Some(header) = (0..=i).rev().find(|&j| paragraphs[j].is_section) else { return i };
100    (header + 1..paragraphs.len()).take_while(|&j| !paragraphs[j].is_section).last().unwrap_or(header)
101}
102
103/// The first and last paragraph a procedure name covers: one paragraph, or a whole section.
104pub(crate) fn procedure(program: &Program, p: &ProcName) -> Result<(usize, usize), String> {
105    let found: Vec<usize> = program
106        .paragraphs
107        .iter()
108        .enumerate()
109        .filter(|(_, q)| q.name == p.name && (p.section.is_none() || (q.section == p.section && !q.is_section)))
110        .map(|(i, _)| i)
111        .collect();
112    match found.as_slice() {
113        [i] if program.paragraphs[*i].is_section => Ok((*i, section_end(program, *i))),
114        [i] => Ok((*i, *i)),
115        [] => Err(format!("no paragraph or section named {}", p.name)),
116        _ => Err(format!("{} names more than one paragraph; qualify it with OF and its section", p.name)),
117    }
118}
119
120impl Compiled {
121    pub fn run(&self, out: &mut dyn Write, err: &mut dyn Write) -> Result<Ending, Abend> {
122        self.run_with(files::Dds::default(), out, err)
123    }
124
125    /// Runs with the DDs that ASSIGN names map to.
126    pub fn run_with(&self, dds: files::Dds, out: &mut dyn Write, err: &mut dyn Write) -> Result<Ending, Abend> {
127        self.execute(unit::Library::default(), dds, None, unit::Clock::System, out, err).map(|(ending, _)| ending)
128    }
129
130    /// Runs as the first program of a run unit: CALL finds other programs in `library`, ACCEPT
131    /// reads `sysin`. Returns how the run ended and RETURN-CODE.
132    pub fn execute<'w>(
133        &self,
134        library: unit::Library,
135        dds: files::Dds,
136        sysin: Option<Box<dyn BufRead + 'w>>,
137        clock: unit::Clock,
138        out: &'w mut dyn Write,
139        err: &'w mut dyn Write,
140    ) -> Result<(Ending, i16), Abend> {
141        let mut run_unit = unit::RunUnit::new(library, dds, sysin, clock, out, err);
142        let me = run_unit.add(None, &self.program, self.layout.size as usize);
143        let ending = machine::Machine::activation(self, me, &mut run_unit, true).and_then(|mut m| m.run_procedure());
144        let closed = run_unit.close_all();
145        let ending = ending?;
146        closed.map_err(|m| Abend { code: "IRONWORK".into(), message: m, pos: Pos::default() })?;
147        Ok((ending, run_unit.return_code()))
148    }
149}
150
151impl Compiled {
152    /// Runs as the first program of a CICS task. `task` says who started it, what COMMAREA it
153    /// starts with, and what files and queues it has. Returns how the run ended and the task, with
154    /// RETURN TRANSID and COMMAREA if it ended that way.
155    pub fn execute_cics<'w>(
156        &self,
157        library: unit::Library,
158        dds: files::Dds,
159        mut task: cics::Task,
160        clock: unit::Clock,
161        out: &'w mut dyn Write,
162        err: &'w mut dyn Write,
163    ) -> Result<(Ending, cics::Task), Abend> {
164        let mut run_unit = unit::RunUnit::new(library, dds, None, clock, out, err);
165        let me = run_unit.add(None, &self.program, self.layout.size as usize);
166        run_unit.eib = run_unit.push_temporary(&[0; cics::EIB_LEN]);
167        let commarea = task.commarea.take();
168        let length = commarea.as_ref().map_or(0, Vec::len);
169        let commarea = commarea.map(|c| run_unit.push_temporary(&c));
170        run_unit.cics = Some(task);
171        let ending = machine::Machine::activation(self, me, &mut run_unit, true).and_then(|mut m| {
172            m.begin_task(commarea, length);
173            m.run_procedure()
174        });
175        let mut closed = run_unit.close_all();
176        for (name, f) in run_unit.cics_files.drain() {
177            if let Err(e) = f.close() {
178                closed = closed.and(Err(format!("closing CICS file {name}: {e}")));
179            }
180        }
181        let mut task = run_unit.cics.take().unwrap_or_default();
182        if let Err(e) = task.flush_td(self.options.code_page()) {
183            closed = closed.and(Err(format!("writing transient data: {e}")));
184        }
185        let ending = ending.map_err(|a| match a.code.strip_prefix("S0C") {
186            Some(_) => Abend { message: format!("{} ({}, which CICS reports as ASRA)", a.message, a.code), code: "ASRA".into(), pos: a.pos },
187            None => a,
188        })?;
189        closed.map_err(|m| Abend { code: "IRONWORK".into(), message: m, pos: Pos::default() })?;
190        Ok((ending, task))
191    }
192}
193
194/// Resolves every name before the program runs, so a misspelling is a compile error.
195struct Check<'a> {
196    layout: &'a Layout,
197    program: &'a Program,
198    errors: &'a mut Vec<Error>,
199}
200
201impl Check<'_> {
202    fn statements(&mut self, stmts: &[Stmt]) {
203        for s in stmts {
204            self.statement(s);
205        }
206    }
207
208    fn statement(&mut self, s: &Stmt) {
209        match s {
210            Stmt::Move { from, to, .. } => {
211                self.operand(from);
212                to.iter().for_each(|r| self.reference(r));
213            }
214            Stmt::Compute { targets, expr, size_error, .. } => {
215                targets.iter().for_each(|t| self.reference(&t.r));
216                self.expr(expr);
217                self.size_error(size_error.as_ref());
218            }
219            Stmt::Arith(a) => {
220                for (t, e) in &a.computations {
221                    self.reference(&t.r);
222                    self.expr(e);
223                }
224                if let Some((t, x, y)) = &a.remainder {
225                    self.reference(&t.r);
226                    self.expr(x);
227                    self.expr(y);
228                }
229                self.size_error(a.size_error.as_ref());
230            }
231            Stmt::If { cond, then, otherwise, .. } => {
232                self.cond(cond);
233                self.statements(then);
234                self.statements(otherwise);
235            }
236            Stmt::PerformInline { body, repeat, .. } => {
237                self.repeat(repeat);
238                self.statements(body);
239            }
240            Stmt::PerformProc { from, thru, repeat, pos } => {
241                self.procedure(from, *pos);
242                if let Some(t) = thru {
243                    self.procedure(t, *pos);
244                }
245                self.repeat(repeat);
246            }
247            Stmt::Evaluate { subjects, whens, other, pos } => {
248                for subject in subjects {
249                    match subject {
250                        Subject::Expr(e) => self.expr(e),
251                        Subject::Cond(c) => self.cond(c),
252                        Subject::Bool(_) => {}
253                    }
254                }
255                for w in whens {
256                    for alternative in &w.alternatives {
257                        for (subject, object) in subjects.iter().zip(alternative) {
258                            match object {
259                                Object::Any => {}
260                                Object::Bool(_) | Object::Cond(_) if matches!(subject, Subject::Expr(_)) => {
261                                    self.errors.push(Error::at(*pos, "a condition as the WHEN object of a value subject"));
262                                }
263                                Object::Bool(_) => {}
264                                Object::Cond(c) => self.cond(c),
265                                Object::Value { .. } if !matches!(subject, Subject::Expr(_)) => {
266                                    self.errors.push(Error::at(*pos, "a value as the WHEN object of a TRUE, FALSE or condition subject"));
267                                }
268                                Object::Value { from, thru, .. } => {
269                                    self.expr(from);
270                                    if let Some(t) = thru {
271                                        self.expr(t);
272                                    }
273                                }
274                            }
275                        }
276                    }
277                    self.statements(&w.body);
278                }
279                self.statements(other);
280            }
281            Stmt::Display { items, .. } => items.iter().for_each(|o| self.operand(o)),
282            Stmt::Open { files, pos } => files.iter().for_each(|(_, f)| self.file(f, *pos)),
283            Stmt::Close { files, pos } => files.iter().for_each(|f| self.file(f, *pos)),
284            Stmt::Read(r) => {
285                self.file(&r.file, r.pos);
286                if r.next {
287                    self.not_random(&r.file, "READ NEXT", r.pos);
288                }
289                if let Some(into) = &r.into {
290                    self.reference(into);
291                }
292                if let Some(key) = &r.key {
293                    self.reference(key);
294                    self.key_of(&r.file, key, false);
295                }
296                self.handlers(&r.at_end);
297                self.handlers(&r.invalid);
298            }
299            Stmt::Write { record, from, invalid, pos, .. } | Stmt::Rewrite { record, from, invalid, pos } => {
300                let verb = if matches!(s, Stmt::Write { .. }) { "WRITE" } else { "REWRITE" };
301                self.reference(record);
302                if let Ok(layout::Resolved::Item(i)) = self.layout.resolve(&record.name, &record.qualifiers, record.pos)
303                    && self.layout.items[i].file.is_none()
304                {
305                    self.errors.push(Error::at(*pos, format!("{verb} {}: not a record of a file", record.name)));
306                }
307                if let Some(op) = from {
308                    self.operand(op);
309                }
310                self.handlers(invalid);
311            }
312            Stmt::Delete { file, invalid, pos } => {
313                self.keyed_file(file, "DELETE", *pos);
314                self.handlers(invalid);
315            }
316            Stmt::Start { file, key, invalid, pos } => {
317                self.keyed_file(file, "START", *pos);
318                self.not_random(file, "START", *pos);
319                if let Some((op, r)) = key {
320                    if !matches!(op, RelOp::Eq | RelOp::Gt | RelOp::Ge) {
321                        self.errors.push(Error::at(*pos, "START KEY takes =, >, NOT < or >="));
322                    }
323                    self.reference(r);
324                    self.key_of(file, r, true);
325                }
326                self.handlers(invalid);
327            }
328            Stmt::Initialize { targets, .. } => targets.iter().for_each(|r| self.reference(r)),
329            Stmt::GoTo { target, pos } => self.procedure(target, *pos),
330            Stmt::Call(c) => {
331                self.operand(&c.target);
332                for arg in &c.using {
333                    if let Some(op) = &arg.value {
334                        self.operand(op);
335                    }
336                }
337                if let Some(r) = &c.returning {
338                    self.reference(r);
339                }
340                self.statements(c.on_exception.as_deref().unwrap_or_default());
341                self.statements(c.not_on_exception.as_deref().unwrap_or_default());
342            }
343            Stmt::Cancel { targets, .. } => targets.iter().for_each(|t| self.operand(t)),
344            Stmt::Set { set, .. } => match set {
345                SetStmt::ConditionTrue(targets) => targets.iter().for_each(|r| self.reference(r)),
346                SetStmt::To { targets, value } | SetStmt::AddressOf { targets, value } => {
347                    targets.iter().for_each(|r| self.reference(r));
348                    self.operand(value);
349                }
350                SetStmt::UpDown { targets, by, .. } => {
351                    targets.iter().for_each(|r| self.reference(r));
352                    self.expr(by);
353                }
354            },
355            Stmt::Accept { target, .. } => self.reference(target),
356            Stmt::String(st) => {
357                for (op, delimiter) in &st.sources {
358                    self.operand(op);
359                    if let Delimiter::By(d) = delimiter {
360                        self.operand(d);
361                    }
362                }
363                self.reference(&st.into);
364                if let Some(p) = &st.pointer {
365                    self.reference(p);
366                }
367                self.statements(st.on_overflow.as_deref().unwrap_or_default());
368                self.statements(st.not_on_overflow.as_deref().unwrap_or_default());
369            }
370            Stmt::Unstring(u) => {
371                self.reference(&u.source);
372                u.delimiters.iter().for_each(|(_, d)| self.operand(d));
373                for into in &u.into {
374                    self.reference(&into.target);
375                    into.delimiter_in.iter().chain(&into.count_in).for_each(|r| self.reference(r));
376                }
377                u.pointer.iter().chain(&u.tallying).for_each(|r| self.reference(r));
378                self.statements(u.on_overflow.as_deref().unwrap_or_default());
379                self.statements(u.not_on_overflow.as_deref().unwrap_or_default());
380            }
381            Stmt::Inspect(i) => {
382                self.reference(&i.target);
383                for p in i.tallying.iter().chain(&i.replacing) {
384                    p.pattern.iter().chain(&p.by).for_each(|o| self.operand(o));
385                    if let Some(c) = &p.counter {
386                        self.reference(c);
387                    }
388                    p.bounds.iter().for_each(|b| self.operand(&b.value));
389                }
390                if let Some((from, to, bounds)) = &i.converting {
391                    self.operand(from);
392                    self.operand(to);
393                    bounds.iter().for_each(|b| self.operand(&b.value));
394                }
395            }
396            Stmt::Search(se) => {
397                self.reference_unsubscripted(&se.table);
398                if let Some(v) = &se.varying {
399                    self.reference(v);
400                }
401                self.statements(se.at_end.as_deref().unwrap_or_default());
402                for (cond, body) in &se.whens {
403                    self.cond(cond);
404                    self.statements(body);
405                }
406            }
407            Stmt::Exec(block) => self.exec_block(block),
408            Stmt::Goback { .. } | Stmt::StopRun { .. } | Stmt::ExitProgram { .. } | Stmt::Continue | Stmt::Exit(_) | Stmt::NextSentence | Stmt::SentenceEnd => {}
409        }
410    }
411
412    fn size_error(&mut self, se: Option<&SizeError>) {
413        if let Some(se) = se {
414            self.statements(&se.on);
415            self.statements(&se.not_on);
416        }
417    }
418
419    fn repeat(&mut self, repeat: &Loop) {
420        match repeat {
421            Loop::Once => {}
422            Loop::Times(e) => self.expr(e),
423            Loop::Until { cond, .. } => self.cond(cond),
424            Loop::Varying { varying, .. } => {
425                self.reference(&varying.var);
426                self.expr(&varying.from);
427                self.expr(&varying.by);
428                self.cond(&varying.until);
429            }
430        }
431    }
432
433    fn file(&mut self, name: &str, pos: Pos) {
434        if !self.program.files.iter().any(|f| f.name == name) {
435            self.errors.push(Error::at(pos, format!("no file named {name}")));
436        }
437    }
438
439    fn keyed_file(&mut self, name: &str, verb: &str, pos: Pos) {
440        match self.program.files.iter().find(|f| f.name == name) {
441            None => self.errors.push(Error::at(pos, format!("no file named {name}"))),
442            Some(f) if !matches!(f.organization, Organization::Indexed | Organization::Relative) => {
443                self.errors.push(Error::at(pos, format!("{verb} {name}: not an indexed or relative file")));
444            }
445            Some(_) => {}
446        }
447    }
448
449    fn not_random(&mut self, name: &str, verb: &str, pos: Pos) {
450        if self.program.files.iter().any(|f| f.name == name && f.access == Access::Random) {
451            self.errors.push(Error::at(pos, format!("{verb} {name}: the file's ACCESS MODE is RANDOM")));
452        }
453    }
454
455    fn handlers(&mut self, h: &Handlers) {
456        self.statements(h.on.as_deref().unwrap_or_default());
457        self.statements(h.not_on.as_deref().unwrap_or_default());
458    }
459
460    fn item(&self, r: &Ref) -> Option<usize> {
461        match self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
462            Ok(layout::Resolved::Item(i)) => Some(i),
463            _ => None,
464        }
465    }
466
467    /// The KEY of READ or START on an indexed file: a record key or alternate key of the file, or
468    /// for START (`partial`) an item that starts where one does and is no longer.
469    fn key_of(&mut self, file: &str, key: &Ref, partial: bool) {
470        let Some(f) = self.program.files.iter().find(|f| f.name == file) else { return };
471        if f.organization != Organization::Indexed {
472            return;
473        }
474        let Some(item) = self.item(key).map(|i| &self.layout.items[i]) else { return };
475        let fits = |r: &Ref| {
476            self.item(r).map(|i| &self.layout.items[i]).is_some_and(|k| k.offset == item.offset && (k.size == item.size || partial && item.size < k.size))
477        };
478        let named = f.record_key.iter().chain(f.alternate_keys.iter().map(|(r, _)| r)).any(fits);
479        if item.file.is_none() || !named {
480            self.errors.push(Error::at(key.pos, format!("{}: not a key of {file}", key.name)));
481        }
482    }
483
484    /// An indexed file's keys lie in its records; a relative file's RELATIVE KEY lies outside them.
485    fn file_keys(&mut self, k: usize) {
486        let f = &self.program.files[k];
487        let in_records = |c: &Self, r: &Ref| c.item(r).is_some_and(|i| c.layout.items[i].file == Some(k as u16));
488        match f.organization {
489            Organization::Indexed => {
490                if f.record_key.is_none() {
491                    self.errors.push(Error::at(f.pos, format!("{}: an indexed file needs a RECORD KEY", f.name)));
492                }
493                for r in f.record_key.iter().chain(f.alternate_keys.iter().map(|(r, _)| r)) {
494                    self.reference(r);
495                    if self.item(r).is_some() && !in_records(self, r) {
496                        self.errors.push(Error::at(r.pos, format!("{}: a key of {} must be in its records", r.name, f.name)));
497                    }
498                }
499            }
500            Organization::Relative => {
501                if let Some(r) = &f.relative_key {
502                    self.reference(r);
503                    if in_records(self, r) {
504                        self.errors.push(Error::at(r.pos, format!("{}: the RELATIVE KEY of {} must not be in its records", r.name, f.name)));
505                    }
506                } else if f.access != Access::Sequential {
507                    self.errors.push(Error::at(f.pos, format!("{}: random or dynamic access needs a RELATIVE KEY", f.name)));
508                }
509            }
510            _ => {}
511        }
512    }
513
514    fn procedure(&mut self, p: &ProcName, pos: Pos) {
515        if let Err(m) = procedure(self.program, p) {
516            self.errors.push(Error::at(pos, m));
517        }
518    }
519
520    fn reference(&mut self, r: &Ref) {
521        if r.name == "RETURN-CODE" && r.qualifiers.is_empty() && self.layout.resolve(&r.name, &r.qualifiers, r.pos).is_err() {
522            return;
523        }
524        match self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
525            Err(e) => self.errors.push(e),
526            Ok(layout::Resolved::Item(i)) if self.layout.items[i].dims.len() != r.subscripts.len() => self.errors.push(Error::at(
527                r.pos,
528                format!("{} takes {} subscripts, not {}", r.name, self.layout.items[i].dims.len(), r.subscripts.len()),
529            )),
530            Ok(_) => {}
531        }
532        r.subscripts.iter().for_each(|e| self.expr(e));
533        if let Some(rm) = &r.refmod {
534            self.expr(&rm.start);
535            if let Some(l) = &rm.length {
536                self.expr(l);
537            }
538        }
539    }
540
541    /// Every host variable and every CICS argument that names data must resolve.
542    fn exec_block(&mut self, block: &ExecBlock) {
543        block.host_variables.iter().for_each(|r| self.reference_unsubscripted(r));
544        for (_, arg) in &block.options {
545            if let Some(ExecArg::Operand(op)) = arg {
546                self.operand(op);
547            }
548        }
549    }
550
551    /// SEARCH names a table without a subscript.
552    fn reference_unsubscripted(&mut self, r: &Ref) {
553        if let Err(e) = self.layout.resolve(&r.name, &r.qualifiers, r.pos) {
554            self.errors.push(e);
555        }
556    }
557
558    fn operand(&mut self, op: &Operand) {
559        match op {
560            Operand::Ref(r) | Operand::LengthOf(r) | Operand::AddressOf(r) => self.reference(r),
561            Operand::Literal(Literal::Number(t)) if machine::literal_fixed(t).is_none() => {
562                self.errors.push(Error::at(Pos::default(), format!("the literal {t} has more than 31 digits")));
563            }
564            Operand::Literal(_) => {}
565            Operand::Function(f) => {
566                if !FUNCTIONS.contains(&f.name.as_str()) {
567                    self.errors.push(Error::at(f.pos, format!("FUNCTION {} is not supported yet", f.name)));
568                }
569                f.args.iter().for_each(|a| self.expr(a));
570            }
571        }
572    }
573
574    fn expr(&mut self, e: &Expr) {
575        match e {
576            Expr::Operand(op) => self.operand(op),
577            Expr::Neg(inner) => self.expr(inner),
578            Expr::Bin(a, _, b) => {
579                self.expr(a);
580                self.expr(b);
581            }
582        }
583    }
584
585    fn cond(&mut self, c: &Cond) {
586        match c {
587            Cond::Rel(a, _, b) => {
588                self.expr(a);
589                self.expr(b);
590            }
591            Cond::Class(e, _) => self.expr(e),
592            Cond::Name(r) => self.reference(r),
593            Cond::NameOrRel { subject, name, .. } => {
594                self.expr(subject);
595                self.reference(name);
596            }
597            Cond::Not(inner) => self.cond(inner),
598            Cond::And(a, b) | Cond::Or(a, b) => {
599                self.cond(a);
600                self.cond(b);
601            }
602        }
603    }
604}
605
606#[cfg(test)]
607mod tests;