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. A reference or pointer can reach anywhere in this memory, but never outside it.
4//!
5//! `H` is the executor's handle to a loaded program and `L` the loader CALL goes through; the run
6//! unit holds both without looking inside, and asks `L` what it needs to know about an `H`.
7
8use crate::abend::Abend;
9use crate::files::{Dds, Open};
10use crate::vocab::{OpenMode, Pos};
11use std::collections::{HashMap, HashSet};
12use std::io::{BufRead, Write};
13use std::path::{Path, PathBuf};
14
15/// A pointer's value is its offset into run-unit memory plus this, so that no item's address is
16/// NULL.
17pub const ADDRESS_BASE: u32 = 0x0001_0000;
18/// RETURN-CODE, a halfword shared by every program in the run unit.
19pub const RETURN_CODE: usize = 0;
20const RESERVED: usize = 8;
21const ALIGNMENT: usize = 8;
22
23pub struct Loaded<H> {
24    /// None for the first program, which the caller of the run unit owns.
25    pub compiled: Option<H>,
26    pub name: String,
27    pub base: usize,
28    pub files: Vec<Option<Open>>,
29    /// The files CLOSE WITH LOCK has closed, which OPEN refuses with status 38.
30    pub locked: Vec<bool>,
31    pub initialized: bool,
32    pub active: bool,
33    /// A dynamic CALL has entered it, so CANCEL acts on it.
34    pub dynamic: bool,
35    /// For a copy a dynamic CALL of an ENTRY name loaded, that entry (numbered as
36    /// [`Loader::entry`] numbers them); None for the program loaded by its PROGRAM-ID.
37    pub entry: Option<usize>,
38    /// Where each paragraph's GO TO goes since an ALTER, by paragraph; empty until one runs.
39    pub altered: Vec<Option<usize>>,
40    /// The library file CALL loaded the program from; None for the programs of the first source.
41    pub source: Option<PathBuf>,
42}
43
44pub enum LoadError {
45    NotFound,
46    Compile(String),
47}
48
49/// A program a loader found and compiled.
50pub struct LoadedProgram<H> {
51    pub compiled: H,
52    /// The PROGRAM-ID, in upper case.
53    pub name: String,
54    pub files: usize,
55    pub size: usize,
56    /// The file it was read from, when a program library supplied it.
57    pub source: Option<PathBuf>,
58}
59
60/// A class definition a loader found and compiled, and its source table, its own source first by
61/// path when a program library supplied it.
62pub struct FoundClass<C> {
63    pub code: C,
64    pub sources: Vec<String>,
65}
66
67/// Where CALL finds programs, and what the run unit needs to know about one it loaded.
68pub trait Loader<H> {
69    /// The executor's handle to a loaded class definition.
70    type Class;
71
72    /// The program whose PROGRAM-ID CALL names, compiled.
73    fn program(&mut self, name: &str) -> Result<LoadedProgram<H>, LoadError>;
74
75    /// The PROGRAM-ID of a program not yet loaded that has an ENTRY of this name.
76    fn holder(&self, entry: &str) -> Option<String>;
77
78    /// Which of a loaded program's ENTRY statements has this name.
79    fn entry(program: &H, name: &str) -> Option<usize>;
80
81    /// A loaded program's file count and storage size.
82    fn shape(program: &H) -> (usize, usize);
83
84    /// The COBOL class definition of this external name, compiled; None for a Java class.
85    fn class(&mut self, external: &str) -> Result<Option<FoundClass<Self::Class>>, String>;
86}
87
88#[derive(Clone, Copy, Debug)]
89pub enum Clock {
90    System,
91    /// Seconds since 1970-01-01T00:00:00Z and hundredths, for runs that must repeat exactly.
92    Fixed(i64, u32),
93}
94
95/// What a run did that its evidence journal records: each file as it is opened and closed, each
96/// program CALL loads, with the source it was read from when a library supplied it, and each
97/// operation an input could steer, with its operand as the program's code page reads it.
98pub enum Event<'a> {
99    Open { dd: &'a str, mode: OpenMode, path: &'a Path },
100    Close { dd: &'a str, path: &'a Path },
101    Load { program: &'a str, source: Option<&'a Path> },
102    /// Control entering paragraph (or section header) `index` of `program` at its start.
103    Paragraph { program: &'a str, name: &'a str, index: usize },
104    /// `kind` is cobolwork's name for the sink (`dynamic-program-load`, `log`, ...); `file` is the
105    /// library file or COPY member the operation is in, empty for the first program's own source.
106    Sink { kind: &'static str, file: &'a str, line: u32, operand: &'a str },
107    /// A statement starting, under [`RunUnit::statements`]; `file` as `Sink`'s.
108    Statement { file: &'a str, line: u32 },
109}
110
111/// The statements whose start a run tells its observer of: every one, or those on these lines of
112/// any source, which the observer narrows to their files.
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub enum StatementFilter {
115    All,
116    Lines(HashSet<u32>),
117}
118
119pub type Observer<'w> = Box<dyn FnMut(Event<'_>) + 'w>;
120
121/// How deep PERFORMs and CALLs may nest before the run abends, rather than exhaust the stack.
122pub const MAX_DEPTH: usize = 100;
123
124/// EXTERNAL data records and file connectors, which belong to the run unit rather than to a
125/// program: one of each name, whichever program first describes it (Language Reference
126/// SC27-8713-03, pp. 65, 184, 197).
127#[derive(Default)]
128pub struct Externals {
129    /// Each EXTERNAL data record, and each EXTERNAL file's record area, by name: where it is and
130    /// how many bytes it has.
131    storage: HashMap<(bool, String), (usize, usize)>,
132    files: Vec<Option<Open>>,
133    /// Whether each EXTERNAL file was closed WITH LOCK.
134    locked: Vec<bool>,
135    file_names: HashMap<String, usize>,
136}
137
138/// A file of a program that is another's file connector.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum Connector {
141    /// The run unit's EXTERNAL file of this number.
142    External(usize),
143    /// File `k` of loaded program `p`: a GLOBAL file of a program containing this one.
144    Program(usize, usize),
145}
146
147/// The routines cobolwork reads a CALL of as running an operating-system command, whose arguments
148/// the input trace checks.
149pub const OS_COMMAND_ROUTINES: &[&str] = &["SYSTEM", "C$SYSTEM", "CBL_EXEC_RUN_UNIT", "CBL_GC_HOSTED", "BXPSYSTM"];
150
151pub struct RunUnit<'w, H, L: Loader<H>> {
152    pub mem: Vec<u8>,
153    /// PERFORMs and CALLs in progress, across every program.
154    pub depth: usize,
155    pub programs: Vec<Loaded<H>>,
156    names: HashMap<String, usize>,
157    pub library: L,
158    pub dds: Dds,
159    pub sysin: Option<Box<dyn BufRead + 'w>>,
160    pub clock: Clock,
161    pub out: &'w mut dyn Write,
162    pub err: &'w mut dyn Write,
163    /// The CICS task a harness run stands in for, with its EXEC interface block and open files.
164    pub cics: Option<crate::cics::Task>,
165    pub eib: usize,
166    pub cics_files: HashMap<String, Open>,
167    /// The database EXEC SQL statements reach, when the run has one.
168    pub sql: Option<crate::sql::Session<'w>>,
169    /// Language Environment's heap storage and message files.
170    pub le: crate::le::State,
171    /// Classes, objects and the JNI environment of the run unit's object-oriented programs.
172    pub oo: crate::oo::Objects<L::Class>,
173    /// Told what the run opens, closes and loads, when a caller keeps evidence of it.
174    pub observer: Option<Observer<'w>>,
175    /// FUNCTION RANDOM's generator, one for the run unit, from the first reference on.
176    pub random: Option<u32>,
177    externals: Externals,
178    /// The files of loaded programs that are another's connector, by program and file.
179    connectors: HashMap<(usize, usize), Connector>,
180    /// The entries SET TO ENTRY has named, which function-pointers and procedure-pointers hold.
181    pub entries: Vec<crate::set::Entry>,
182    /// The statements an observer is told of as each starts; None tells it of none.
183    pub statements: Option<StatementFilter>,
184}
185
186impl<'w, H: Clone, L: Loader<H>> RunUnit<'w, H, L> {
187    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 {
188        Self {
189            mem: vec![0; RESERVED],
190            depth: 0,
191            programs: Vec::new(),
192            names: HashMap::new(),
193            library,
194            dds,
195            sysin,
196            clock,
197            out,
198            err,
199            cics: None,
200            eib: 0,
201            cics_files: HashMap::new(),
202            sql: None,
203            le: crate::le::State::default(),
204            oo: Default::default(),
205            observer: None,
206            random: None,
207            externals: Externals::default(),
208            connectors: HashMap::new(),
209            entries: Vec::new(),
210            statements: None,
211        }
212    }
213
214    fn allocate(&mut self, size: usize) -> usize {
215        let base = self.mem.len().div_ceil(ALIGNMENT) * ALIGNMENT;
216        self.mem.resize(base + size, 0);
217        base
218    }
219
220    /// Adds a program to the run unit under `name` and gives it its storage.
221    pub fn add_named(&mut self, compiled: Option<H>, name: String, files: usize, size: usize) -> usize {
222        let base = self.allocate(size);
223        let index = self.programs.len();
224        self.names.insert(name.clone(), index);
225        self.programs.push(Loaded { compiled, name, base, files: (0..files).map(|_| None).collect(), locked: vec![false; files], initialized: false, active: false, dynamic: false, entry: None, altered: Vec::new(), source: None });
226        index
227    }
228
229    /// The program a CALL of `name` enters, and which of its ENTRY statements when `name` is not
230    /// its PROGRAM-ID. A static CALL of an entry name enters the one copy of the program; a dynamic
231    /// CALL gets a copy of its own for each entry name (assumption C51).
232    pub fn load_entry(&mut self, name: &str, dynamic: bool) -> Result<(usize, Option<usize>), LoadError> {
233        let name = name.to_ascii_uppercase();
234        if let Some(i) = self.find(&name) {
235            return Ok((i, self.programs[i].entry));
236        }
237        let index = match self.programs.iter().position(|p| p.compiled.as_ref().is_some_and(|c| L::entry(c, &name).is_some())) {
238            Some(i) => i,
239            None => {
240                let holder = self.library.holder(&name);
241                self.load(holder.as_deref().unwrap_or(&name))?
242            }
243        };
244        let Some(compiled) = self.programs[index].compiled.clone() else { return Ok((index, None)) };
245        let Some(entry) = L::entry(&compiled, &name) else { return Ok((index, None)) };
246        if !dynamic {
247            return Ok((index, Some(entry)));
248        }
249        let (files, size) = L::shape(&compiled);
250        let copy = self.add_named(Some(compiled), name, files, size);
251        self.programs[copy].entry = Some(entry);
252        self.programs[copy].source = self.programs[index].source.clone();
253        Ok((copy, Some(entry)))
254    }
255
256    /// Storage for a BY CONTENT or BY VALUE argument, at the end of memory.
257    pub fn push_temporary(&mut self, bytes: &[u8]) -> usize {
258        let at = self.allocate(bytes.len());
259        self.mem[at..at + bytes.len()].copy_from_slice(bytes);
260        at
261    }
262
263    /// Releases arguments pushed since `mark`, unless a program, heap storage or EXTERNAL storage
264    /// was placed behind them.
265    pub fn release_temporaries(&mut self, mark: usize) {
266        let external = self.externals.storage.values().all(|&(at, _)| at < mark);
267        if self.programs.iter().all(|p| p.base < mark) && self.le.heap_end() <= mark && external {
268            self.mem.truncate(mark.max(RESERVED));
269        }
270    }
271
272    /// One more PERFORM or CALL in progress, refused past `MAX_DEPTH` rather than exhaust the stack.
273    pub fn enter(&mut self, pos: Pos) -> Result<(), Abend> {
274        if self.depth >= MAX_DEPTH {
275            return Err(Abend::ironwork(format!("PERFORM and CALL nest deeper than {MAX_DEPTH}"), pos));
276        }
277        self.depth += 1;
278        Ok(())
279    }
280
281    /// Marks program `me` active: where its storage starts, and whether the activation starts from
282    /// fresh storage, as its first does, the first after a CANCEL, and every one of an INITIAL
283    /// program.
284    pub fn activate(&mut self, me: usize, initial: bool) -> (usize, bool) {
285        let program = &mut self.programs[me];
286        program.active = true;
287        (program.base, !program.initialized || initial)
288    }
289
290    /// Program `me`'s storage holds its initial values, and its GO TOs go where they are written.
291    pub fn initialized(&mut self, me: usize) {
292        self.programs[me].initialized = true;
293        self.programs[me].altered.clear();
294    }
295
296    pub fn find(&self, name: &str) -> Option<usize> {
297        self.names.get(&name.to_ascii_uppercase()).copied()
298    }
299
300    /// The program CALL names, compiling and loading it the first time.
301    pub fn load(&mut self, name: &str) -> Result<usize, LoadError> {
302        let name = name.to_ascii_uppercase();
303        if let Some(i) = self.find(&name) {
304            return Ok(i);
305        }
306        let loaded = self.library.program(&name)?;
307        self.notify(Event::Load { program: &name, source: loaded.source.as_deref() });
308        let index = self.add_named(Some(loaded.compiled), loaded.name, loaded.files, loaded.size);
309        self.programs[index].source = loaded.source;
310        Ok(index)
311    }
312
313    pub const fn observed(&self) -> bool {
314        self.observer.is_some()
315    }
316
317    /// Whether a statement starting on `line` is told to the observer.
318    pub fn traces(&self, line: u32) -> bool {
319        match &self.statements {
320            None => false,
321            Some(StatementFilter::All) => self.observer.is_some(),
322            Some(StatementFilter::Lines(lines)) => self.observer.is_some() && lines.contains(&line),
323        }
324    }
325
326    pub fn notify(&mut self, event: Event<'_>) {
327        if let Some(observer) = self.observer.as_mut() {
328            observer(event);
329        }
330    }
331
332    /// Closes every file any program left open, as the runtime does when the run unit ends.
333    pub fn close_all(&mut self) -> Result<(), String> {
334        for program in &mut self.programs {
335            for f in program.files.iter_mut().filter_map(Option::take) {
336                f.close().map_err(|e| format!("closing a file of {}: {e}", program.name))?;
337            }
338        }
339        for (name, &k) in &self.externals.file_names {
340            if let Some(f) = self.externals.files[k].take() {
341                f.close().map_err(|e| format!("closing EXTERNAL file {name}: {e}"))?;
342            }
343        }
344        Ok(())
345    }
346
347    /// Where EXTERNAL record `name` is, or EXTERNAL file `name`'s record area when `file`: storage
348    /// of `size` bytes, zeroed, the first time a program describes it. A description of another
349    /// size is refused (assumption C180).
350    pub fn external(&mut self, name: &str, file: bool, size: usize) -> Result<usize, String> {
351        let key = (file, name.to_owned());
352        if let Some(&(at, had)) = self.externals.storage.get(&key) {
353            return if had == size {
354                Ok(at)
355            } else {
356                let what = if file { "the record area of EXTERNAL file" } else { "EXTERNAL record" };
357                Err(format!("{what} {name} has {had} bytes in the run unit, and this program describes {size}"))
358            };
359        }
360        let at = self.allocate(size);
361        self.externals.storage.insert(key, (at, size));
362        Ok(at)
363    }
364
365    /// The run unit's connector for EXTERNAL file `name`.
366    pub fn external_file(&mut self, name: &str) -> Connector {
367        let (files, locked) = (&mut self.externals.files, &mut self.externals.locked);
368        let k = *self.externals.file_names.entry(name.to_owned()).or_insert_with(|| {
369            files.push(None);
370            locked.push(false);
371            files.len() - 1
372        });
373        Connector::External(k)
374    }
375
376    /// Program `me`'s file k is the connector `to`, for as long as the run unit lasts.
377    pub fn connect(&mut self, me: usize, k: usize, to: Connector) {
378        self.connectors.insert((me, k), to);
379    }
380
381    fn connector(&self, mut me: usize, mut k: usize) -> Option<Connector> {
382        let mut to = None;
383        while let Some(&c) = self.connectors.get(&(me, k)) {
384            to = Some(c);
385            match c {
386                Connector::External(_) => break,
387                Connector::Program(p, j) => (me, k) = (p, j),
388            }
389        }
390        to
391    }
392
393    /// Program `me`'s file k, open or not: its own, or the connector it shares.
394    pub fn file(&mut self, me: usize, k: usize) -> &mut Option<Open> {
395        match self.connector(me, k) {
396            None => &mut self.programs[me].files[k],
397            Some(Connector::External(e)) => &mut self.externals.files[e],
398            Some(Connector::Program(p, j)) => &mut self.programs[p].files[j],
399        }
400    }
401
402    /// Whether program `me`'s file k, or the connector it shares, was closed WITH LOCK.
403    pub fn locked(&mut self, me: usize, k: usize) -> &mut bool {
404        match self.connector(me, k) {
405            None => &mut self.programs[me].locked[k],
406            Some(Connector::External(e)) => &mut self.externals.locked[e],
407            Some(Connector::Program(p, j)) => &mut self.programs[p].locked[j],
408        }
409    }
410
411    pub fn file_ref(&self, me: usize, k: usize) -> &Option<Open> {
412        match self.connector(me, k) {
413            None => &self.programs[me].files[k],
414            Some(Connector::External(e)) => &self.externals.files[e],
415            Some(Connector::Program(p, j)) => &self.programs[p].files[j],
416        }
417    }
418
419    pub fn return_code(&self) -> i16 {
420        i16::from_be_bytes([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]])
421    }
422
423    /// The current time: seconds since the epoch, and hundredths.
424    pub fn now(&self) -> (i64, u32) {
425        match self.clock {
426            Clock::Fixed(s, h) => (s, h),
427            Clock::System => {
428                let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
429                (d.as_secs() as i64, d.subsec_millis() / 10)
430            }
431        }
432    }
433}