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::PathBuf;
10use std::rc::Rc;
11use syntax::ast::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}
31
32/// Where CALL finds programs: the other programs of the first program's source, then program
33/// libraries searched by member name.
34#[derive(Clone, Debug, Default)]
35pub struct Library {
36    pub programs: Vec<Program>,
37    pub dirs: Vec<PathBuf>,
38    pub copy: copy::Libraries,
39    pub flags: Vec<String>,
40}
41
42pub enum LoadError {
43    NotFound,
44    Compile(String),
45}
46
47#[derive(Clone, Copy, Debug)]
48pub enum Clock {
49    System,
50    /// Seconds since 1970-01-01T00:00:00Z and hundredths, for runs that must repeat exactly.
51    Fixed(i64, u32),
52}
53
54/// How deep PERFORMs and CALLs may nest before the run abends, rather than exhaust the stack.
55pub const MAX_DEPTH: usize = 100;
56
57pub struct RunUnit<'w> {
58    pub mem: Vec<u8>,
59    /// PERFORMs and CALLs in progress, across every program.
60    pub depth: usize,
61    pub programs: Vec<Loaded>,
62    names: HashMap<String, usize>,
63    library: Library,
64    pub dds: Dds,
65    pub sysin: Option<Box<dyn BufRead + 'w>>,
66    pub clock: Clock,
67    pub out: &'w mut dyn Write,
68    pub err: &'w mut dyn Write,
69    /// The CICS task a harness run stands in for, with its EXEC interface block and open files.
70    pub cics: Option<crate::cics::Task>,
71    pub eib: usize,
72    pub cics_files: HashMap<String, Open>,
73}
74
75fn member_name(name: &str) -> bool {
76    !name.is_empty() && name.len() <= 30 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '#' || c == '$')
77}
78
79impl<'w> RunUnit<'w> {
80    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 {
81        Self {
82            mem: vec![0; RESERVED],
83            depth: 0,
84            programs: Vec::new(),
85            names: HashMap::new(),
86            library,
87            dds,
88            sysin,
89            clock,
90            out,
91            err,
92            cics: None,
93            eib: 0,
94            cics_files: HashMap::new(),
95        }
96    }
97
98    fn allocate(&mut self, size: usize) -> usize {
99        let base = self.mem.len().div_ceil(ALIGNMENT) * ALIGNMENT;
100        self.mem.resize(base + size, 0);
101        base
102    }
103
104    /// Adds a program to the run unit and gives it its storage.
105    pub fn add(&mut self, compiled: Option<Rc<Compiled>>, program: &Program, size: usize) -> usize {
106        let base = self.allocate(size);
107        let index = self.programs.len();
108        let name = program.id.to_ascii_uppercase();
109        self.names.insert(name.clone(), index);
110        self.programs.push(Loaded { compiled, name, base, files: program.files.iter().map(|_| None).collect(), initialized: false, active: false });
111        index
112    }
113
114    /// Storage for a BY CONTENT or BY VALUE argument, at the end of memory.
115    pub fn push_temporary(&mut self, bytes: &[u8]) -> usize {
116        let at = self.allocate(bytes.len());
117        self.mem[at..at + bytes.len()].copy_from_slice(bytes);
118        at
119    }
120
121    /// Releases arguments pushed since `mark`, unless a program was loaded behind them.
122    pub fn release_temporaries(&mut self, mark: usize) {
123        if self.programs.iter().all(|p| p.base < mark) {
124            self.mem.truncate(mark.max(RESERVED));
125        }
126    }
127
128    pub fn find(&self, name: &str) -> Option<usize> {
129        self.names.get(&name.to_ascii_uppercase()).copied()
130    }
131
132    /// The program CALL names, compiling and loading it the first time.
133    pub fn load(&mut self, name: &str) -> Result<usize, LoadError> {
134        let name = name.to_ascii_uppercase();
135        if let Some(i) = self.find(&name) {
136            return Ok(i);
137        }
138        if !member_name(&name) {
139            return Err(LoadError::NotFound);
140        }
141        let program = match self.library.programs.iter().position(|p| p.id.eq_ignore_ascii_case(&name)) {
142            Some(i) => self.library.programs.remove(i),
143            None => self.search_libraries(&name)?,
144        };
145        let compiled = crate::compile(program, &self.library.flags).map_err(|errors| {
146            let first = errors.first().map(|e| e.place(&name)).unwrap_or_default();
147            LoadError::Compile(format!("{name} does not compile: {first}"))
148        })?;
149        let compiled = Rc::new(compiled);
150        let size = compiled.layout.size as usize;
151        let program = compiled.program.clone();
152        Ok(self.add(Some(compiled), &program, size))
153    }
154
155    fn search_libraries(&mut self, name: &str) -> Result<Program, LoadError> {
156        let candidates = [name.to_owned(), name.to_ascii_lowercase()];
157        let path = self
158            .library
159            .dirs
160            .iter()
161            .flat_map(|d| candidates.iter().flat_map(move |n| ["", ".cbl", ".CBL", ".cob", ".COB"].iter().map(move |e| d.join(format!("{n}{e}")))))
162            .find(|p| p.is_file())
163            .ok_or(LoadError::NotFound)?;
164        let text = std::fs::read(&path).map(|b| copy::decode(&b)).map_err(|e| LoadError::Compile(format!("{}: {e}", path.display())))?;
165        let mut programs = syntax::parse_all_with(&text, &self.library.copy)
166            .map_err(|e| LoadError::Compile(format!("{name} does not compile: {}", e.place(&path.display().to_string()))))?;
167        let first = programs.remove(0);
168        self.library.programs.extend(programs);
169        Ok(first)
170    }
171
172    /// Closes every file any program left open, as the runtime does when the run unit ends.
173    pub fn close_all(&mut self) -> Result<(), String> {
174        for program in &mut self.programs {
175            for f in program.files.iter_mut().filter_map(Option::take) {
176                f.close().map_err(|e| format!("closing a file of {}: {e}", program.name))?;
177            }
178        }
179        Ok(())
180    }
181
182    pub fn copy_libraries(&self) -> &copy::Libraries {
183        &self.library.copy
184    }
185
186    pub fn return_code(&self) -> i16 {
187        i16::from_be_bytes([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]])
188    }
189
190    /// The current time: seconds since the epoch, and hundredths.
191    pub fn now(&self) -> (i64, u32) {
192        match self.clock {
193            Clock::Fixed(s, h) => (s, h),
194            Clock::System => {
195                let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
196                (d.as_secs() as i64, d.subsec_millis() / 10)
197            }
198        }
199    }
200}
201
202/// Calendar fields of a UTC time: year, month, day, hour, minute, second, day of year (1-based),
203/// and day of week (1 Monday to 7 Sunday).
204pub fn civil(seconds: i64) -> (i64, u32, u32, u32, u32, u32, u32, u32) {
205    let days = seconds.div_euclid(86_400);
206    let secs = seconds.rem_euclid(86_400) as u32;
207    let z = days + 719_468;
208    let era = z.div_euclid(146_097);
209    let doe = z.rem_euclid(146_097);
210    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
211    let doy_march = doe - (365 * yoe + yoe / 4 - yoe / 100);
212    let mp = (5 * doy_march + 2) / 153;
213    let day = (doy_march - (153 * mp + 2) / 5 + 1) as u32;
214    let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
215    let year = yoe + era * 400 + i64::from(month <= 2);
216    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
217    let before = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month as usize - 1] + u32::from(leap && month > 2);
218    let weekday = ((days + 3).rem_euclid(7) + 1) as u32;
219    (year, month, day, secs / 3600, secs / 60 % 60, secs % 60, before + day, weekday)
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn civil_dates() {
228        assert_eq!(civil(0), (1970, 1, 1, 0, 0, 0, 1, 4));
229        let t = 1_790_510_400;
230        let (y, m, d, _, _, _, yday, wday) = civil(t);
231        assert_eq!((y, m, d, yday, wday), (2026, 9, 27, 270, 7));
232        assert_eq!(civil(951_782_400).6, 60);
233    }
234}