Skip to main content

ironwork_exec/
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
5use crate::files::{Dds, Open};
6use crate::Compiled;
7use std::collections::HashMap;
8use std::io::{BufRead, Write};
9use std::path::{Path, PathBuf};
10use std::rc::Rc;
11use syntax::ast::{OpenMode, Program};
12use syntax::copy;
13
14/// A pointer's value is its offset into run-unit memory plus this, so that no item's address is
15/// NULL.
16pub const ADDRESS_BASE: u32 = 0x0001_0000;
17/// RETURN-CODE, a halfword shared by every program in the run unit.
18pub const RETURN_CODE: usize = 0;
19const RESERVED: usize = 8;
20const ALIGNMENT: usize = 8;
21
22pub struct Loaded {
23    /// None for the first program, which the caller of the run unit owns.
24    pub compiled: Option<Rc<Compiled>>,
25    pub name: String,
26    pub base: usize,
27    pub files: Vec<Option<Open>>,
28    pub initialized: bool,
29    pub active: bool,
30    /// For a copy a dynamic CALL of an ENTRY name loaded, that entry (numbered as
31    /// [`Compiled::entries`] numbers them); None for the program loaded by its PROGRAM-ID.
32    pub entry: Option<usize>,
33    /// Where each paragraph's GO TO goes since an ALTER, by paragraph; empty until one runs.
34    pub altered: Vec<Option<usize>>,
35}
36
37/// Where CALL finds programs: the other programs of the first program's source, then program
38/// libraries searched by member name.
39#[derive(Clone, Debug, Default)]
40pub struct Library {
41    pub programs: Vec<Program>,
42    pub dirs: Vec<PathBuf>,
43    pub copy: copy::Libraries,
44    pub flags: Vec<String>,
45}
46
47pub enum LoadError {
48    NotFound,
49    Compile(String),
50}
51
52#[derive(Clone, Copy, Debug)]
53pub enum Clock {
54    System,
55    /// Seconds since 1970-01-01T00:00:00Z and hundredths, for runs that must repeat exactly.
56    Fixed(i64, u32),
57}
58
59/// What a run did that its evidence journal records: each file as it is opened and closed, and
60/// each program CALL loads, with the source it was read from when a library supplied it.
61pub enum Event<'a> {
62    Open { dd: &'a str, mode: OpenMode, path: &'a Path },
63    Close { dd: &'a str, path: &'a Path },
64    Load { program: &'a str, source: Option<&'a Path> },
65}
66
67pub type Observer<'w> = Box<dyn FnMut(Event<'_>) + 'w>;
68
69/// How deep PERFORMs and CALLs may nest before the run abends, rather than exhaust the stack.
70pub const MAX_DEPTH: usize = 100;
71
72pub struct RunUnit<'w> {
73    pub mem: Vec<u8>,
74    /// PERFORMs and CALLs in progress, across every program.
75    pub depth: usize,
76    pub programs: Vec<Loaded>,
77    names: HashMap<String, usize>,
78    pub(crate) library: Library,
79    pub dds: Dds,
80    pub sysin: Option<Box<dyn BufRead + 'w>>,
81    pub clock: Clock,
82    pub out: &'w mut dyn Write,
83    pub err: &'w mut dyn Write,
84    /// The CICS task a harness run stands in for, with its EXEC interface block and open files.
85    pub cics: Option<crate::cics::Task>,
86    pub eib: usize,
87    pub cics_files: HashMap<String, Open>,
88    /// The database EXEC SQL statements reach, when the run has one.
89    pub sql: Option<crate::sql::Session<'w>>,
90    /// Language Environment's heap storage and message files.
91    pub le: crate::le::State,
92    /// Classes, objects and the JNI environment of the run unit's object-oriented programs.
93    pub oo: crate::oo::Objects,
94    /// Told what the run opens, closes and loads, when a caller keeps evidence of it.
95    pub observer: Option<Observer<'w>>,
96    /// FUNCTION RANDOM's generator, one for the run unit, from the first reference on.
97    pub random: Option<u32>,
98}
99
100fn member_name(name: &str) -> bool {
101    !name.is_empty() && name.len() <= 30 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '#' || c == '$')
102}
103
104impl<'w> RunUnit<'w> {
105    pub fn new(library: Library, dds: Dds, sysin: Option<Box<dyn BufRead + 'w>>, clock: Clock, out: &'w mut dyn Write, err: &'w mut dyn Write) -> Self {
106        Self {
107            mem: vec![0; RESERVED],
108            depth: 0,
109            programs: Vec::new(),
110            names: HashMap::new(),
111            library,
112            dds,
113            sysin,
114            clock,
115            out,
116            err,
117            cics: None,
118            eib: 0,
119            cics_files: HashMap::new(),
120            sql: None,
121            le: crate::le::State::default(),
122            oo: Default::default(),
123            observer: None,
124            random: None,
125        }
126    }
127
128    fn allocate(&mut self, size: usize) -> usize {
129        let base = self.mem.len().div_ceil(ALIGNMENT) * ALIGNMENT;
130        self.mem.resize(base + size, 0);
131        base
132    }
133
134    /// Adds a program to the run unit and gives it its storage.
135    pub fn add(&mut self, compiled: Option<Rc<Compiled>>, program: &Program, size: usize) -> usize {
136        self.add_named(compiled, program.id.to_ascii_uppercase(), program.files.len(), size)
137    }
138
139    fn add_named(&mut self, compiled: Option<Rc<Compiled>>, name: String, files: usize, size: usize) -> usize {
140        let base = self.allocate(size);
141        let index = self.programs.len();
142        self.names.insert(name.clone(), index);
143        self.programs.push(Loaded { compiled, name, base, files: (0..files).map(|_| None).collect(), initialized: false, active: false, entry: None, altered: Vec::new() });
144        index
145    }
146
147    /// The program a CALL of `name` enters, and which of its ENTRY statements when `name` is not
148    /// its PROGRAM-ID. A static CALL of an entry name enters the one copy of the program; a dynamic
149    /// CALL gets a copy of its own for each entry name (assumption C51).
150    pub fn load_entry(&mut self, name: &str, dynamic: bool) -> Result<(usize, Option<usize>), LoadError> {
151        let name = name.to_ascii_uppercase();
152        if let Some(i) = self.find(&name) {
153            return Ok((i, self.programs[i].entry));
154        }
155        let index = match self.programs.iter().position(|p| p.compiled.as_ref().is_some_and(|c| c.entries.iter().any(|e| e.name == name))) {
156            Some(i) => i,
157            None => {
158                let holder = self.library.programs.iter().find(|p| crate::entry_points(p).iter().any(|e| e.name == name)).map(|p| p.id.to_ascii_uppercase());
159                self.load(holder.as_deref().unwrap_or(&name))?
160            }
161        };
162        let Some(compiled) = self.programs[index].compiled.clone() else { return Ok((index, None)) };
163        let Some(entry) = compiled.entries.iter().position(|e| e.name == name) else { return Ok((index, None)) };
164        if !dynamic {
165            return Ok((index, Some(entry)));
166        }
167        let (files, size) = (compiled.program.files.len(), compiled.layout.size as usize);
168        let copy = self.add_named(Some(compiled), name, files, size);
169        self.programs[copy].entry = Some(entry);
170        Ok((copy, Some(entry)))
171    }
172
173    /// Storage for a BY CONTENT or BY VALUE argument, at the end of memory.
174    pub fn push_temporary(&mut self, bytes: &[u8]) -> usize {
175        let at = self.allocate(bytes.len());
176        self.mem[at..at + bytes.len()].copy_from_slice(bytes);
177        at
178    }
179
180    /// Releases arguments pushed since `mark`, unless a program or heap storage was placed behind them.
181    pub fn release_temporaries(&mut self, mark: usize) {
182        if self.programs.iter().all(|p| p.base < mark) && self.le.heap_end() <= mark {
183            self.mem.truncate(mark.max(RESERVED));
184        }
185    }
186
187    pub fn find(&self, name: &str) -> Option<usize> {
188        self.names.get(&name.to_ascii_uppercase()).copied()
189    }
190
191    /// The program CALL names, compiling and loading it the first time.
192    pub fn load(&mut self, name: &str) -> Result<usize, LoadError> {
193        let name = name.to_ascii_uppercase();
194        if let Some(i) = self.find(&name) {
195            return Ok(i);
196        }
197        if !member_name(&name) {
198            return Err(LoadError::NotFound);
199        }
200        let (program, source) = match self.library.programs.iter().position(|p| p.id.eq_ignore_ascii_case(&name)) {
201            Some(i) => (self.library.programs.remove(i), None),
202            None => self.search_libraries(&name).map(|(p, path)| (p, Some(path)))?,
203        };
204        let compiled = crate::compile(program, &self.library.flags).map_err(|errors| {
205            let first = syntax::most_severe(&errors).map(|e| e.place(&name)).unwrap_or_default();
206            LoadError::Compile(format!("{name} does not compile: {first}"))
207        })?;
208        self.notify(Event::Load { program: &name, source: source.as_deref() });
209        let compiled = Rc::new(compiled);
210        let size = compiled.layout.size as usize;
211        let program = compiled.program.clone();
212        Ok(self.add(Some(compiled), &program, size))
213    }
214
215    pub(crate) fn notify(&mut self, event: Event<'_>) {
216        if let Some(observer) = self.observer.as_mut() {
217            observer(event);
218        }
219    }
220
221    fn search_libraries(&mut self, name: &str) -> Result<(Program, PathBuf), LoadError> {
222        let candidates = [name.to_owned(), name.to_ascii_lowercase()];
223        let path = self
224            .library
225            .dirs
226            .iter()
227            .flat_map(|d| candidates.iter().flat_map(move |n| ["", ".cbl", ".CBL", ".cob", ".COB"].iter().map(move |e| d.join(format!("{n}{e}")))))
228            .find(|p| p.is_file())
229            .ok_or(LoadError::NotFound)?;
230        let text = std::fs::read(&path).map(|b| copy::decode(&b)).map_err(|e| LoadError::Compile(format!("{}: {e}", path.display())))?;
231        let mut programs = syntax::parse_all_with(&text, &self.library.copy.with_program(&path))
232            .map_err(|e| LoadError::Compile(format!("{name} does not compile: {}", e.place(&path.display().to_string()))))?;
233        let first = programs.remove(0);
234        self.library.programs.extend(programs);
235        Ok((first, path))
236    }
237
238    /// Closes every file any program left open, as the runtime does when the run unit ends.
239    pub fn close_all(&mut self) -> Result<(), String> {
240        for program in &mut self.programs {
241            for f in program.files.iter_mut().filter_map(Option::take) {
242                f.close().map_err(|e| format!("closing a file of {}: {e}", program.name))?;
243            }
244        }
245        Ok(())
246    }
247
248    pub fn copy_libraries(&self) -> &copy::Libraries {
249        &self.library.copy
250    }
251
252    pub fn return_code(&self) -> i16 {
253        i16::from_be_bytes([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]])
254    }
255
256    /// The current time: seconds since the epoch, and hundredths.
257    pub fn now(&self) -> (i64, u32) {
258        match self.clock {
259            Clock::Fixed(s, h) => (s, h),
260            Clock::System => {
261                let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
262                (d.as_secs() as i64, d.subsec_millis() / 10)
263            }
264        }
265    }
266}