Skip to main content

ironwork_rt/
unit.rs

1//! The run unit: every program a run calls, sharing one memory as they share an address space on
2//! z/OS. A called program keeps its WORKING-STORAGE and open files from one CALL to the next until
3//! it is cancelled, or in a CICS task until the LINK or XCTL that started its run unit ends it
4//! (C145). A reference or pointer can reach anywhere in this memory, but never outside it.
5//!
6//! `H` is the executor's handle to a loaded program and `L` the loader CALL goes through; the run
7//! unit holds both without looking inside, and asks `L` what it needs to know about an `H`.
8
9use crate::abend::Abend;
10use crate::files::{Dds, Open};
11use crate::oo::ClassCode;
12use crate::storage::Loc;
13use crate::taint::Taint;
14use crate::vocab::{OpenMode, Pos};
15use std::collections::{HashMap, HashSet};
16use std::io::{BufRead, Write};
17use std::path::{Path, PathBuf};
18use std::rc::Rc;
19
20/// A pointer's value is its offset into run-unit memory plus this, so that no item's address is
21/// NULL.
22pub const ADDRESS_BASE: u32 = 0x0001_0000;
23/// RETURN-CODE, a halfword shared by every program in the run unit.
24pub const RETURN_CODE: usize = 0;
25const RESERVED: usize = 8;
26const ALIGNMENT: usize = 8;
27
28pub struct Loaded<H> {
29    /// None for the first program, which the caller of the run unit owns.
30    pub compiled: Option<H>,
31    pub name: String,
32    pub base: usize,
33    pub size: usize,
34    /// False once a CICS run unit has set the program's storage at `base` aside, or has ended and
35    /// released it: its next activation gets storage of its own.
36    pub placed: bool,
37    pub files: Vec<Option<Open>>,
38    /// The files CLOSE WITH LOCK has closed, which OPEN refuses with status 38.
39    pub locked: Vec<bool>,
40    pub initialized: bool,
41    pub active: bool,
42    /// A dynamic CALL has entered it, so CANCEL acts on it.
43    pub dynamic: bool,
44    /// For a copy a dynamic CALL of an ENTRY name loaded, that entry (numbered as
45    /// [`Loader::entry`] numbers them); None for the program loaded by its PROGRAM-ID.
46    pub entry: Option<usize>,
47    /// Where each paragraph's GO TO goes since an ALTER, by paragraph; empty until one runs.
48    pub altered: Vec<Option<usize>>,
49    /// The library file CALL loaded the program from; None for the programs of the first source.
50    pub source: Option<PathBuf>,
51}
52
53impl<H> Loaded<H> {
54    /// A program with `files` files and `size` bytes of storage at `base`, in its initial state.
55    pub fn new(compiled: Option<H>, name: String, base: usize, size: usize, files: usize) -> Self {
56        let (locked, files) = (vec![false; files], (0..files).map(|_| None).collect());
57        Self { compiled, name, base, size, placed: true, files, locked, initialized: false, active: false, dynamic: false, entry: None, altered: Vec::new(), source: None }
58    }
59
60    /// What the program's activations have left, taken away: it is in its initial state, with its
61    /// storage set aside.
62    fn set_aside(&mut self) -> Held {
63        let files = self.files.iter_mut().map(Option::take).collect();
64        let locked = std::mem::replace(&mut self.locked, vec![false; self.files.len()]);
65        let held = Held { base: self.base, placed: self.placed, files, locked, initialized: self.initialized, active: self.active, dynamic: self.dynamic, altered: std::mem::take(&mut self.altered) };
66        (self.placed, self.initialized, self.active, self.dynamic) = (false, false, false, false);
67        held
68    }
69
70    fn restore(&mut self, held: Held) {
71        (self.base, self.placed, self.files, self.locked, self.altered) = (held.base, held.placed, held.files, held.locked, held.altered);
72        (self.initialized, self.active, self.dynamic) = (held.initialized, held.active, held.dynamic);
73    }
74}
75
76/// What a CICS run unit holds that the LINK or XCTL starting another sets aside until it ends:
77/// each program's state, the EXTERNAL data and files with the connectors to them, the Language
78/// Environment heap (C126), FUNCTION RANDOM's sequence (C104) and RETURN-CODE (C105), which
79/// belong to the enclave.
80struct Enclave {
81    programs: Vec<Held>,
82    externals: Externals,
83    connectors: HashMap<(usize, usize), Connector>,
84    heap: Vec<(usize, usize, bool)>,
85    random: Option<u32>,
86    /// RETURN-CODE's bytes, and whether they may hold input.
87    return_code: ([u8; 2], bool),
88}
89
90/// A program's state in a CICS run unit that a LINK or XCTL has set aside.
91struct Held {
92    base: usize,
93    placed: bool,
94    files: Vec<Option<Open>>,
95    locked: Vec<bool>,
96    initialized: bool,
97    active: bool,
98    dynamic: bool,
99    altered: Vec<Option<usize>>,
100}
101
102pub enum LoadError {
103    NotFound,
104    /// Found, but its source does not compile or its load module does not load.
105    Compile(String),
106}
107
108/// A program a loader found and compiled.
109pub struct LoadedProgram<H> {
110    pub compiled: H,
111    /// The PROGRAM-ID, in upper case.
112    pub name: String,
113    pub files: usize,
114    pub size: usize,
115    /// The file it was read from, when a program library supplied it.
116    pub source: Option<PathBuf>,
117}
118
119/// A class definition a loader found and compiled, and its source table, its own source first by
120/// path when a program library supplied it.
121pub struct FoundClass<C> {
122    pub code: C,
123    pub sources: Vec<String>,
124}
125
126/// Where CALL finds programs, and what the run unit needs to know about one it loaded.
127pub trait Loader<H> {
128    /// The program whose PROGRAM-ID CALL names, compiled.
129    fn program(&mut self, name: &str) -> Result<LoadedProgram<H>, LoadError>;
130
131    /// The PROGRAM-ID of a program not yet loaded that has an ENTRY of this name.
132    fn holder(&self, entry: &str) -> Option<String>;
133
134    /// Which of a loaded program's ENTRY statements has this name.
135    fn entry(program: &H, name: &str) -> Option<usize>;
136
137    /// A loaded program's file count and storage size.
138    fn shape(program: &H) -> (usize, usize);
139
140    /// The PROGRAM-IDs of the programs a loaded program contains, which a CANCEL of it reaches.
141    fn nested(program: &H) -> &[String];
142
143    /// Source file `file` of a loaded program's source table, by name.
144    fn source(program: &H, file: usize) -> Option<String>;
145
146    /// The COBOL class definition of this external name, its data and methods each a program the
147    /// executor runs; None for a Java class.
148    fn class(&mut self, external: &str) -> Result<Option<FoundClass<Rc<ClassCode<H>>>>, String>;
149
150    /// A BMS mapset from the copy libraries, which SEND MAP and RECEIVE MAP read; None when no
151    /// library holds it.
152    fn mapset(&mut self, name: &str) -> Option<Result<crate::bms::Mapset, String>>;
153}
154
155/// An executor's activation, as each service's host trait reaches the run unit through it: `Program`
156/// is the executor's handle to a loaded program, `Loader` the loader CALL goes through.
157pub trait UnitHost<'w> {
158    type Program: Clone;
159    type Loader: Loader<Self::Program>;
160    fn unit(&mut self) -> &mut RunUnit<'w, Self::Program, Self::Loader>;
161}
162
163#[derive(Clone, Copy, Debug)]
164pub enum Clock {
165    System,
166    /// Seconds since 1970-01-01T00:00:00Z and hundredths, for runs that must repeat exactly.
167    Fixed(i64, u32),
168}
169
170/// What a run did that its evidence journal records: each file as it is opened and closed, each
171/// program CALL loads, with the source it was read from when a library supplied it, and each
172/// operation an input could steer, with its operand as the program's code page reads it.
173pub enum Event<'a> {
174    Open { dd: &'a str, mode: OpenMode, path: &'a Path },
175    Close { dd: &'a str, path: &'a Path },
176    Load { program: &'a str, source: Option<&'a Path> },
177    /// Control entering paragraph (or section header) `index` of `program` at its start.
178    Paragraph { program: &'a str, name: &'a str, index: usize },
179    /// `kind` is cobolwork's name for the sink (`dynamic-program-load`, `log`, ...); `file` is the
180    /// library file or COPY member the operation is in, empty for the first program's own source;
181    /// `input`, under [`RunUnit::taint`], whether an input byte may be in the operand
182    /// ([`crate::taint::Taint::at_sink`]).
183    Sink { kind: &'static str, file: &'a str, line: u32, operand: &'a str, input: Option<bool> },
184    /// A statement starting, under [`RunUnit::statements`]; `file` as `Sink`'s.
185    Statement { file: &'a str, line: u32 },
186}
187
188/// The statements whose start a run tells its observer of: every one, or those on these lines of
189/// any source, which the observer narrows to their files.
190#[derive(Clone, Debug, PartialEq, Eq)]
191pub enum StatementFilter {
192    All,
193    Lines(HashSet<u32>),
194}
195
196pub type Observer<'w> = Box<dyn FnMut(Event<'_>) + 'w>;
197
198/// How deep PERFORMs and CALLs may nest before the run abends, rather than exhaust the stack.
199pub const MAX_DEPTH: usize = 100;
200
201/// EXTERNAL data records and file connectors, which belong to the run unit rather than to a
202/// program: one of each name, whichever program first describes it (Language Reference
203/// SC27-8713-03, pp. 65, 184, 197).
204#[derive(Default)]
205pub struct Externals {
206    /// Each EXTERNAL data record, and each EXTERNAL file's record area, by name: where it is and
207    /// how many bytes it has.
208    storage: HashMap<(bool, String), (usize, usize)>,
209    files: Vec<Option<Open>>,
210    /// Whether each EXTERNAL file was closed WITH LOCK.
211    locked: Vec<bool>,
212    file_names: HashMap<String, usize>,
213}
214
215/// A file of a program that is another's file connector.
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub enum Connector {
218    /// The run unit's EXTERNAL file of this number.
219    External(usize),
220    /// File `k` of loaded program `p`: a GLOBAL file of a program containing this one.
221    Program(usize, usize),
222}
223
224/// The routines cobolwork reads a CALL of as running an operating-system command, whose arguments
225/// the input trace checks.
226pub const OS_COMMAND_ROUTINES: &[&str] = &["SYSTEM", "C$SYSTEM", "CBL_EXEC_RUN_UNIT", "CBL_GC_HOSTED", "BXPSYSTM"];
227
228pub struct RunUnit<'w, H, L: Loader<H>> {
229    pub mem: Vec<u8>,
230    /// PERFORMs and CALLs in progress, across every program.
231    pub depth: usize,
232    pub programs: Vec<Loaded<H>>,
233    names: HashMap<String, usize>,
234    pub library: L,
235    pub dds: Dds,
236    pub sysin: Option<Box<dyn BufRead + 'w>>,
237    pub clock: Clock,
238    pub out: &'w mut dyn Write,
239    pub err: &'w mut dyn Write,
240    /// The CICS task a harness run stands in for, with its EXEC interface block and open files.
241    pub cics: Option<crate::cics::Task>,
242    pub eib: usize,
243    pub cics_files: HashMap<String, Open>,
244    /// The database EXEC SQL statements reach, when the run has one.
245    pub sql: Option<crate::sql::Session<'w>>,
246    /// Language Environment's heap storage and message files.
247    pub le: crate::le::State,
248    /// Classes, objects and the JNI environment of the run unit's object-oriented programs.
249    pub oo: crate::oo::Objects<Rc<ClassCode<H>>>,
250    /// Told what the run opens, closes and loads, when a caller keeps evidence of it.
251    pub observer: Option<Observer<'w>>,
252    /// FUNCTION RANDOM's generator, one for the run unit, from the first reference on.
253    pub random: Option<u32>,
254    externals: Externals,
255    /// The files of loaded programs that are another's connector, by program and file.
256    connectors: HashMap<(usize, usize), Connector>,
257    /// The entries SET TO ENTRY has named, which function-pointers and procedure-pointers hold.
258    pub entries: Vec<crate::set::Entry>,
259    /// The statements an observer is told of as each starts; None tells it of none.
260    pub statements: Option<StatementFilter>,
261    /// Which bytes may hold input, when the run traces input.
262    pub taint: Option<Taint>,
263    /// How many more statements may start before the run ends with S322; None for no limit.
264    pub statement_limit: Option<u64>,
265    /// The CICS run units the LINKs and XCTLs running have set aside, the innermost last.
266    set_aside: Vec<Enclave>,
267}
268
269fn end_file(f: Open, unclosed: bool) -> std::io::Result<()> {
270    if unclosed { f.abandon() } else { f.close() }
271}
272
273impl<H, L: Loader<H>> RunUnit<'_, H, L> {
274    /// Copies `bytes` into memory at `offset`; under taint they may hold input when the running
275    /// statement has read a byte that may. Every write of data to memory goes through here or
276    /// [`RunUnit::write_input`], or marks its bytes with [`RunUnit::mark`].
277    pub fn write(&mut self, offset: usize, bytes: &[u8]) {
278        self.mem[offset..offset + bytes.len()].copy_from_slice(bytes);
279        self.mark(offset, bytes.len());
280    }
281
282    /// Copies input into memory at `offset`: bytes a READ, ACCEPT or row brought in.
283    pub fn write_input(&mut self, offset: usize, bytes: &[u8]) {
284        self.mem[offset..offset + bytes.len()].copy_from_slice(bytes);
285        self.mark_input(offset, bytes.len(), true);
286    }
287
288    /// Marks bytes written outside [`RunUnit::write`] as it would.
289    pub fn mark(&mut self, offset: usize, len: usize) {
290        if let Some(t) = self.taint.as_mut() {
291            let pending = t.pending();
292            t.set(offset, len, pending);
293        }
294    }
295
296    /// Marks bytes as input, or as holding none: initial values, which are constants.
297    pub fn mark_input(&mut self, offset: usize, len: usize, input: bool) {
298        if let Some(t) = self.taint.as_mut() {
299            t.set(offset, len, input);
300        }
301    }
302
303    /// A read of `loc` by the running statement.
304    pub fn taint_read(&mut self, loc: Loc) {
305        if let Some(t) = self.taint.as_mut() {
306            t.read(loc.offset, loc.len);
307        }
308    }
309
310    /// [`Taint::writing`], when the run traces input.
311    pub fn writing(&mut self, on: bool) -> bool {
312        self.taint.as_mut().is_some_and(|t| t.writing(on))
313    }
314
315    /// A statement with a position starts: its writes carry only what it reads.
316    pub fn statement_starts(&mut self) {
317        if let Some(t) = self.taint.as_mut() {
318            t.start_statement();
319        }
320    }
321
322    /// [`Taint::take_input`], when the run traces input.
323    pub fn take_input(&mut self) {
324        if let Some(t) = self.taint.as_mut() {
325            t.take_input();
326        }
327    }
328
329    /// Whether any byte of the range may hold input; false without taint.
330    pub fn holds_input(&self, offset: usize, len: usize) -> bool {
331        self.taint.as_ref().is_some_and(|t| t.any(offset, len))
332    }
333
334    /// Whether the running statement has read a byte that may hold input.
335    pub fn pending(&self) -> bool {
336        self.taint.as_ref().is_some_and(Taint::pending)
337    }
338
339    /// [`Taint::resume_statement`], when the run traces input.
340    pub fn resume_statement(&mut self, read_before: bool) {
341        if let Some(t) = self.taint.as_mut() {
342            t.resume_statement(read_before);
343        }
344    }
345
346    /// The run did `what`, which taint does not follow.
347    pub fn unfollowed(&mut self, what: &'static str) {
348        if let Some(t) = self.taint.as_mut() {
349            t.unfollowed(what);
350        }
351    }
352
353    /// Whether an input byte may be in a sink's operand; None without taint.
354    pub fn input_at_sink(&self) -> Option<bool> {
355        self.taint.as_ref().and_then(Taint::at_sink)
356    }
357}
358
359impl<'w, H: Clone, L: Loader<H>> RunUnit<'w, H, L> {
360    pub fn new(library: L, dds: Dds, sysin: Option<Box<dyn BufRead + 'w>>, clock: Clock, out: &'w mut dyn Write, err: &'w mut dyn Write) -> Self {
361        Self {
362            mem: vec![0; RESERVED],
363            depth: 0,
364            programs: Vec::new(),
365            names: HashMap::new(),
366            library,
367            dds,
368            sysin,
369            clock,
370            out,
371            err,
372            cics: None,
373            eib: 0,
374            cics_files: HashMap::new(),
375            sql: None,
376            le: crate::le::State::default(),
377            oo: Default::default(),
378            observer: None,
379            random: None,
380            externals: Externals::default(),
381            connectors: HashMap::new(),
382            entries: Vec::new(),
383            statements: None,
384            taint: None,
385            statement_limit: None,
386            set_aside: Vec::new(),
387        }
388    }
389
390    fn allocate(&mut self, size: usize) -> usize {
391        let base = self.mem.len().div_ceil(ALIGNMENT) * ALIGNMENT;
392        self.mem.resize(base + size, 0);
393        base
394    }
395
396    /// Adds a program to the run unit under `name` and gives it its storage.
397    pub fn add_named(&mut self, compiled: Option<H>, name: String, files: usize, size: usize) -> usize {
398        let base = self.allocate(size);
399        let index = self.programs.len();
400        self.names.insert(name.clone(), index);
401        self.programs.push(Loaded::new(compiled, name, base, size, files));
402        index
403    }
404
405    /// A CICS LINK or XCTL starts a run unit of its own (C145): every program starts in it in its
406    /// initial state, with storage of its own, it has no EXTERNAL data or files and an empty heap
407    /// (C126), FUNCTION RANDOM has not been referenced (C104) and RETURN-CODE is zero (C105), and
408    /// the state of the run unit that issued it is set aside until [`RunUnit::end_cics_run_unit`].
409    pub fn begin_cics_run_unit(&mut self) {
410        let programs = self.programs.iter_mut().map(Loaded::set_aside).collect();
411        let (externals, connectors) = (std::mem::take(&mut self.externals), std::mem::take(&mut self.connectors));
412        let return_code = ([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]], self.holds_input(RETURN_CODE, 2));
413        self.mem[RETURN_CODE..RETURN_CODE + 2].fill(0);
414        self.mark_input(RETURN_CODE, 2, false);
415        let (heap, random) = (std::mem::take(&mut self.le.heap), self.random.take());
416        self.set_aside.push(Enclave { programs, externals, connectors, heap, random, return_code });
417    }
418
419    /// Ends the run unit [`RunUnit::begin_cics_run_unit`] started, closing the files its programs
420    /// and its EXTERNAL files left open as Language Environment closes an enclave's, and dropping
421    /// its programs' storage, EXTERNAL data and heap: a program it loaded stays loaded, in its
422    /// initial state with no storage (C129), and the run unit set aside has everything back but,
423    /// after `xctl`, RETURN-CODE, the program XCTL started having taken the issuer's place (C105).
424    pub fn end_cics_run_unit(&mut self, xctl: bool) -> Result<(), String> {
425        let mut closed = Ok(());
426        for program in &mut self.programs {
427            for f in program.files.iter_mut().filter_map(Option::take) {
428                if let Err(e) = f.close() {
429                    closed = closed.and(Err(format!("closing a file of {}: {e}", program.name)));
430                }
431            }
432            drop(program.set_aside());
433        }
434        closed = closed.and(self.close_external_files(false));
435        let Some(enclave) = self.set_aside.pop() else { return closed };
436        for (program, held) in self.programs.iter_mut().zip(enclave.programs) {
437            program.restore(held);
438        }
439        (self.externals, self.connectors, self.le.heap, self.random) = (enclave.externals, enclave.connectors, enclave.heap, enclave.random);
440        if !xctl {
441            let (bytes, input) = enclave.return_code;
442            self.mem[RETURN_CODE..RETURN_CODE + 2].copy_from_slice(&bytes);
443            self.mark_input(RETURN_CODE, 2, input);
444        }
445        closed
446    }
447
448    /// The program a CALL of `name` enters, and which of its ENTRY statements when `name` is not
449    /// its PROGRAM-ID. A static CALL of an entry name enters the one copy of the program; a dynamic
450    /// CALL gets a copy of its own for each entry name (assumption C51).
451    pub fn load_entry(&mut self, name: &str, dynamic: bool) -> Result<(usize, Option<usize>), LoadError> {
452        let name = name.to_ascii_uppercase();
453        if let Some(i) = self.find(&name) {
454            return Ok((i, self.programs[i].entry));
455        }
456        let index = match self.programs.iter().position(|p| p.compiled.as_ref().is_some_and(|c| L::entry(c, &name).is_some())) {
457            Some(i) => i,
458            None => {
459                let holder = self.library.holder(&name);
460                self.load(holder.as_deref().unwrap_or(&name))?
461            }
462        };
463        let Some(compiled) = self.programs[index].compiled.clone() else { return Ok((index, None)) };
464        let Some(entry) = L::entry(&compiled, &name) else { return Ok((index, None)) };
465        if !dynamic {
466            return Ok((index, Some(entry)));
467        }
468        let (files, size) = L::shape(&compiled);
469        let copy = self.add_named(Some(compiled), name, files, size);
470        self.programs[copy].entry = Some(entry);
471        self.programs[copy].source = self.programs[index].source.clone();
472        Ok((copy, Some(entry)))
473    }
474
475    /// Storage for a BY CONTENT or BY VALUE argument, at the end of memory, written as
476    /// [`RunUnit::write`] writes.
477    pub fn push_temporary(&mut self, bytes: &[u8]) -> usize {
478        let at = self.allocate(bytes.len());
479        self.write(at, bytes);
480        at
481    }
482
483    /// Releases arguments pushed since `mark`, unless a program's storage, heap storage or EXTERNAL
484    /// storage was placed behind them.
485    pub fn release_temporaries(&mut self, mark: usize) {
486        let external = self.externals.storage.values().all(|&(at, _)| at < mark);
487        if self.programs.iter().all(|p| !p.placed || p.base + p.size <= mark) && self.le.heap_end() <= mark && external {
488            self.mem.truncate(mark.max(RESERVED));
489            if let Some(t) = self.taint.as_mut() {
490                t.truncate(self.mem.len());
491            }
492        }
493    }
494
495    /// One more PERFORM or CALL in progress, refused past `MAX_DEPTH` rather than exhaust the stack.
496    pub fn enter(&mut self, pos: Pos) -> Result<(), Abend> {
497        if self.depth >= MAX_DEPTH {
498            return Err(Abend::ironwork(format!("PERFORM and CALL nest deeper than {MAX_DEPTH}"), pos));
499        }
500        self.depth += 1;
501        Ok(())
502    }
503
504    /// Marks program `me` active: where its storage starts, and whether the activation starts from
505    /// fresh storage, as its first does, the first after a CANCEL, and every one of an INITIAL
506    /// program.
507    pub fn activate(&mut self, me: usize, initial: bool) -> (usize, bool) {
508        if !self.programs[me].placed {
509            let base = self.allocate(self.programs[me].size);
510            (self.programs[me].base, self.programs[me].placed) = (base, true);
511        }
512        let program = &mut self.programs[me];
513        program.active = true;
514        (program.base, !program.initialized || initial)
515    }
516
517    /// Program `me`'s storage holds its initial values, and its GO TOs go where they are written.
518    pub fn initialized(&mut self, me: usize) {
519        self.programs[me].initialized = true;
520        self.programs[me].altered.clear();
521    }
522
523    pub fn find(&self, name: &str) -> Option<usize> {
524        self.names.get(&name.to_ascii_uppercase()).copied()
525    }
526
527    /// The program CALL names, compiling and loading it the first time.
528    pub fn load(&mut self, name: &str) -> Result<usize, LoadError> {
529        let name = name.to_ascii_uppercase();
530        if let Some(i) = self.find(&name) {
531            return Ok(i);
532        }
533        let loaded = self.library.program(&name)?;
534        self.notify(Event::Load { program: &name, source: loaded.source.as_deref() });
535        let index = self.add_named(Some(loaded.compiled), loaded.name, loaded.files, loaded.size);
536        self.programs[index].source = loaded.source;
537        Ok(index)
538    }
539
540    pub const fn observed(&self) -> bool {
541        self.observer.is_some()
542    }
543
544    /// Whether a statement starting on `line` is told to the observer.
545    /// Counts the start of the statement at `pos` against the statement limit: once it is spent the
546    /// run ends there with S322, as z/OS ends a step that runs past its TIME= (assumption C241).
547    pub fn start_statement(&mut self, pos: Pos) -> Result<(), Abend> {
548        match self.statement_limit.as_mut() {
549            Some(0) => Err(Abend { code: crate::abend::AbendCode::TimeLimit, message: "the run reached its statement limit, as a step past its TIME= ends".into(), pos, file: None }),
550            Some(left) => {
551                *left -= 1;
552                Ok(())
553            }
554            None => Ok(()),
555        }
556    }
557
558    pub fn traces(&self, line: u32) -> bool {
559        match &self.statements {
560            None => false,
561            Some(StatementFilter::All) => self.observer.is_some(),
562            Some(StatementFilter::Lines(lines)) => self.observer.is_some() && lines.contains(&line),
563        }
564    }
565
566    pub fn notify(&mut self, event: Event<'_>) {
567        if let Some(observer) = self.observer.as_mut() {
568            observer(event);
569        }
570    }
571
572    /// Closes every file any program left open, as the runtime does when the run unit ends, normally
573    /// or by an abend under TRAP(ON). An abend TRAP(OFF) keeps from Language Environment closes
574    /// none (`unclosed`), and each VSAM data set stays marked open for output
575    /// ([`numeric::assumptions::TRAP_OFF_LEAVES_FILES_OPEN`]).
576    pub fn close_all(&mut self, unclosed: bool) -> Result<(), String> {
577        for program in &mut self.programs {
578            for f in program.files.iter_mut().filter_map(Option::take) {
579                end_file(f, unclosed).map_err(|e| format!("closing a file of {}: {e}", program.name))?;
580            }
581        }
582        self.close_external_files(unclosed)
583    }
584
585    /// Closes the EXTERNAL files left open, giving the first failure.
586    fn close_external_files(&mut self, unclosed: bool) -> Result<(), String> {
587        let mut closed = Ok(());
588        for (name, &k) in &self.externals.file_names {
589            if let Some(f) = self.externals.files[k].take()
590                && let Err(e) = end_file(f, unclosed)
591            {
592                closed = closed.and(Err(format!("closing EXTERNAL file {name}: {e}")));
593            }
594        }
595        closed
596    }
597
598    /// Where EXTERNAL record `name` is, or EXTERNAL file `name`'s record area when `file`: storage
599    /// of `size` bytes, zeroed, the first time a program describes it. A description of another
600    /// size is refused (assumption C180).
601    pub fn external(&mut self, name: &str, file: bool, size: usize) -> Result<usize, String> {
602        let key = (file, name.to_owned());
603        if let Some(&(at, had)) = self.externals.storage.get(&key) {
604            return if had == size {
605                Ok(at)
606            } else {
607                let what = if file { "the record area of EXTERNAL file" } else { "EXTERNAL record" };
608                Err(format!("{what} {name} has {had} bytes in the run unit, and this program describes {size}"))
609            };
610        }
611        let at = self.allocate(size);
612        self.externals.storage.insert(key, (at, size));
613        Ok(at)
614    }
615
616    /// The run unit's connector for EXTERNAL file `name`.
617    pub fn external_file(&mut self, name: &str) -> Connector {
618        let (files, locked) = (&mut self.externals.files, &mut self.externals.locked);
619        let k = *self.externals.file_names.entry(name.to_owned()).or_insert_with(|| {
620            files.push(None);
621            locked.push(false);
622            files.len() - 1
623        });
624        Connector::External(k)
625    }
626
627    /// Program `me`'s file k is the connector `to`, for as long as the run unit lasts.
628    pub fn connect(&mut self, me: usize, k: usize, to: Connector) {
629        self.connectors.insert((me, k), to);
630    }
631
632    fn connector(&self, mut me: usize, mut k: usize) -> Option<Connector> {
633        let mut to = None;
634        while let Some(&c) = self.connectors.get(&(me, k)) {
635            to = Some(c);
636            match c {
637                Connector::External(_) => break,
638                Connector::Program(p, j) => (me, k) = (p, j),
639            }
640        }
641        to
642    }
643
644    /// Program `me`'s file k, open or not: its own, or the connector it shares.
645    pub fn file(&mut self, me: usize, k: usize) -> &mut Option<Open> {
646        match self.connector(me, k) {
647            None => &mut self.programs[me].files[k],
648            Some(Connector::External(e)) => &mut self.externals.files[e],
649            Some(Connector::Program(p, j)) => &mut self.programs[p].files[j],
650        }
651    }
652
653    /// Whether program `me`'s file k, or the connector it shares, was closed WITH LOCK.
654    pub fn locked(&mut self, me: usize, k: usize) -> &mut bool {
655        match self.connector(me, k) {
656            None => &mut self.programs[me].locked[k],
657            Some(Connector::External(e)) => &mut self.externals.locked[e],
658            Some(Connector::Program(p, j)) => &mut self.programs[p].locked[j],
659        }
660    }
661
662    pub fn file_ref(&self, me: usize, k: usize) -> &Option<Open> {
663        match self.connector(me, k) {
664            None => &self.programs[me].files[k],
665            Some(Connector::External(e)) => &self.externals.files[e],
666            Some(Connector::Program(p, j)) => &self.programs[p].files[j],
667        }
668    }
669
670    pub fn return_code(&self) -> i16 {
671        i16::from_be_bytes([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]])
672    }
673
674    /// The current time: seconds since the epoch, and hundredths.
675    pub fn now(&self) -> (i64, u32) {
676        match self.clock {
677            Clock::Fixed(s, h) => (s, h),
678            Clock::System => {
679                let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
680                (d.as_secs() as i64, d.subsec_millis() / 10)
681            }
682        }
683    }
684}