ironwork-rt 0.3.0

ironwork for COBOL: the runtime a compiled program links, under the runtime exception
Documentation
//! A CALL that reaches a Language Environment callable service. The service gets its arguments as
//! addresses and reads or writes each at its documented length, whatever the argument's own; its
//! feedback code goes to fc, or, with fc OMITTED, a failure ends the run as an unhandled condition.

use super::{Condition, Reading, Stamp};
use crate::abend::{Abend, AbendCode};
use crate::calendar;
use crate::lir::LeService;
use crate::unit::{ADDRESS_BASE, Loader, RunUnit};
use crate::vocab::Pos;
use std::io::Write;
use zarch::ebcdic::CodePage;

/// The largest CEEGTST request the run grants, as GETMAIN's limit is for CICS.
const HEAP_LIMIT: usize = 1 << 28;
/// MSGFILE's default ddname, where CEEMOUT writes.
const MSGFILE: &str = "SYSOUT";
/// The transient data queue that takes a CICS task's LE messages and dumps in place of any DD.
const CESE: &str = "CESE";

type R<T> = Result<T, Abend>;
type Outcome = R<Option<Condition>>;

/// What a service asks of the executor running it: the run unit, the program's code page, and the
/// name a dump lists a loaded program by.
pub trait LeHost<'w> {
    type Program: Clone;
    type Loader: Loader<Self::Program>;
    fn unit(&mut self) -> &mut RunUnit<'w, Self::Program, Self::Loader>;
    fn page(&self) -> &'static CodePage;
    /// `Class.method` for a loaded COBOL method; None for a program, which a dump lists by name.
    fn method_name(program: &Self::Program) -> Option<String>;
}

/// Runs `service` with each argument's address in run-unit memory, as a CALL to a program passes it.
pub fn call<'w, X: LeHost<'w>>(x: &mut X, service: LeService, args: &[Option<usize>], pos: Pos) -> R<()> {
    let page = x.page();
    Services { unit: x.unit(), page, method_name: X::method_name }.le_service(service, args, pos)
}

struct Services<'u, 'w, H, L: Loader<H>> {
    unit: &'u mut RunUnit<'w, H, L>,
    page: &'static CodePage,
    method_name: fn(&H) -> Option<String>,
}

struct LeCall<'a> {
    name: &'a str,
    args: &'a [Option<usize>],
    pos: Pos,
}

impl<H: Clone, L: Loader<H>> Services<'_, '_, H, L> {
    fn le_service(&mut self, service: LeService, args: &[Option<usize>], pos: Pos) -> R<()> {
        let name = super::name(service);
        let params = super::parameters(name).unwrap_or_default();
        let required = params.iter().filter(|p| **p != "fc").count();
        if args.len() < params.len() {
            let passed = match args.len() {
                1 => "1 argument".to_owned(),
                n => format!("{n} arguments"),
            };
            return Err(Abend::ironwork(format!("CALL {name} passes {passed}; {name} takes {}, and with fewer z/OS is unpredictable", params.join(", ")), pos));
        }
        let call = LeCall { name, args, pos };
        let failed = match service {
            LeService::Cee3abd => return Err(self.cee3abd(&call)),
            LeService::Cee3dmp => self.cee3dmp(&call)?,
            LeService::Ceedate => self.ceedate(&call)?,
            LeService::Ceedatm => self.ceedatm(&call)?,
            LeService::Ceedays => self.ceedays_or_secs(&call, Reading::Days)?,
            LeService::Ceedywk => self.ceedywk(&call)?,
            LeService::Ceefrst => self.ceefrst(&call)?,
            LeService::Ceegmt | LeService::Ceeutc => self.ceegmt(&call)?,
            LeService::Ceegmto => self.ceegmto(&call)?,
            LeService::Ceegtst => self.ceegtst(&call)?,
            LeService::Ceeloct => self.ceeloct(&call)?,
            LeService::Ceemout => self.ceemout(&call)?,
            LeService::Ceesecs => self.ceedays_or_secs(&call, Reading::Seconds)?,
        };
        match (args.get(required).copied().flatten(), failed) {
            (Some(fc), failed) => {
                self.le_store(fc, &failed.map_or([0; 12], Condition::token));
                Ok(())
            }
            (None, Some(c)) if c.severity >= 2 => Err(Abend {
                code: AbendCode::user(4038),
                message: format!("CALL {name}: {} {} With its feedback code omitted the condition was signaled, and nothing handled it", c.symbol(), c.text()),
                pos,
                file: None,
            }),
            (None, _) => Ok(()),
        }
    }

    /// The address of parameter `i`; an OMITTED one is a null address the service stores through.
    fn le_at(&self, call: &LeCall, i: usize) -> R<usize> {
        call.args.get(i).copied().flatten().ok_or_else(|| {
            let param = super::parameters(call.name).and_then(|p| p.get(i)).copied().unwrap_or("argument");
            Abend { code: AbendCode::Protection, message: format!("CALL {}: {param} is OMITTED, and the service addresses it", call.name), pos: call.pos, file: None }
        })
    }

    fn le_load(&self, at: usize, len: usize, pos: Pos) -> R<Vec<u8>> {
        let bytes = at.checked_add(len).and_then(|end| self.unit.mem.get(at..end));
        bytes.map(<[u8]>::to_vec).ok_or_else(|| Abend { code: AbendCode::Protection, message: "a callable service's argument reaches outside the run unit's storage".into(), pos, file: None })
    }

    /// Stores at an argument's address; what would fall past the run unit's storage is not kept.
    fn le_store(&mut self, at: usize, bytes: &[u8]) {
        let end = (at + bytes.len()).min(self.unit.mem.len());
        if at < end {
            self.unit.mem[at..end].copy_from_slice(&bytes[..end - at]);
        }
    }

    fn le_fullword(&self, call: &LeCall, i: usize) -> R<i32> {
        let at = self.le_at(call, i)?;
        let b = self.le_load(at, 4, call.pos)?;
        Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
    }

    /// A halfword length-prefixed string; a negative length is a null string.
    fn le_vstring(&self, call: &LeCall, i: usize) -> R<Vec<u8>> {
        let at = self.le_at(call, i)?;
        let len = self.le_load(at, 2, call.pos)?;
        let len = i16::from_be_bytes([len[0], len[1]]).max(0) as usize;
        self.le_load(at + 2, len, call.pos)
    }

    fn le_output(&mut self, call: &LeCall, i: usize, bytes: &[u8]) -> R<()> {
        let at = self.le_at(call, i)?;
        self.le_store(at, bytes);
        Ok(())
    }

    fn le_now(&self) -> Stamp {
        let (seconds, hundredths) = self.unit.now();
        Stamp::from_unix(seconds, hundredths)
    }

    fn cee3abd(&mut self, call: &LeCall) -> Abend {
        let code = match self.le_fullword(call, 0) {
            Ok(c) => c,
            Err(a) => return a,
        };
        let clean_up = self.le_fullword(call, 1).ok().filter(|t| (0..=5).contains(t));
        let how = match clean_up {
            Some(1) => "with normal enclave termination",
            Some(2..=5) => "with enclave termination and its dumps as clean-up asks",
            _ => "without clean-up",
        };
        let user = ((code as u32) & 0xFFF) as u16;
        if self.unit.cics.is_some() {
            return Abend { code: AbendCode::Cics(format!("{user:04}")), message: format!("CALL CEE3ABD: transaction abend {user:04}"), pos: call.pos, file: None };
        }
        Abend { code: AbendCode::user(user), message: format!("CALL CEE3ABD: user abend {user} {how}"), pos: call.pos, file: None }
    }

    fn ceedays_or_secs(&mut self, call: &LeCall, reading: Reading) -> Outcome {
        let input = self.le_vstring(call, 0)?;
        let picture = self.le_vstring(call, 1)?;
        let window = self.le_now().fields().year - 80;
        let result = super::read(&input, &picture, reading, window, self.page);
        let failed = result.err();
        let bytes = match (reading, result) {
            (Reading::Days, r) => (r.map_or(0, |s| s.lilian) as i32).to_be_bytes().to_vec(),
            (Reading::Seconds, r) => r.map_or([0; 8], |s| super::seconds_hfp(s.total_millis())).to_vec(),
        };
        self.le_output(call, 2, &bytes)?;
        Ok(failed)
    }

    fn ceedate(&mut self, call: &LeCall) -> Outcome {
        let lilian = self.le_fullword(call, 0)?;
        let picture = self.le_vstring(call, 1)?;
        let (text, failed) = super::date(i64::from(lilian), &picture, self.page);
        self.le_output(call, 2, &text)?;
        Ok(failed)
    }

    fn ceedatm(&mut self, call: &LeCall) -> Outcome {
        let at = self.le_at(call, 0)?;
        let seconds = self.le_load(at, 8, call.pos)?;
        let picture = self.le_vstring(call, 1)?;
        let (text, failed) = super::timestamp(seconds.try_into().unwrap_or([0; 8]), &picture, self.page);
        self.le_output(call, 2, &text)?;
        Ok(failed)
    }

    fn ceedywk(&mut self, call: &LeCall) -> Outcome {
        let lilian = i64::from(self.le_fullword(call, 0)?);
        let (day, failed) = if (1..=calendar::LAST_LILIAN).contains(&lilian) { (calendar::weekday(lilian) as i32, None) } else { (0, Some(super::LILIAN_RANGE)) };
        self.le_output(call, 1, &day.to_be_bytes())?;
        Ok(failed)
    }

    fn ceegmt(&mut self, call: &LeCall) -> Outcome {
        let now = self.le_now();
        self.le_output(call, 0, &(now.lilian as i32).to_be_bytes())?;
        self.le_output(call, 1, &super::seconds_hfp(now.total_millis()))?;
        Ok(None)
    }

    fn ceeloct(&mut self, call: &LeCall) -> Outcome {
        let now = self.le_now();
        self.le_output(call, 0, &(now.lilian as i32).to_be_bytes())?;
        self.le_output(call, 1, &super::seconds_hfp(now.total_millis()))?;
        let text = self.page.encode(&now.gregorian()).unwrap_or_default();
        self.le_output(call, 2, &text)?;
        Ok(None)
    }

    fn ceegmto(&mut self, call: &LeCall) -> Outcome {
        self.le_output(call, 0, &[0; 4])?;
        self.le_output(call, 1, &[0; 4])?;
        self.le_output(call, 2, &[0; 8])?;
        Ok(None)
    }

    fn ceemout(&mut self, call: &LeCall) -> Outcome {
        let message = self.le_vstring(call, 0)?;
        if self.le_fullword(call, 1)? != 2 {
            return Ok(Some(super::DESTINATION));
        }
        let text = self.page.decode(&message);
        self.le_write(MSGFILE, &[text], call.pos)?;
        Ok(None)
    }

    fn cee3dmp(&mut self, call: &LeCall) -> Outcome {
        let at = self.le_at(call, 0)?;
        let title = self.page.decode(&self.le_load(at, 80, call.pos)?);
        let at = self.le_at(call, 1)?;
        let options = self.page.decode(&self.le_load(at, 255, call.pos)?);
        let (dd, invalid) = super::dump_options(&options);
        let now = self.le_now().fields();
        let method_name = self.method_name;
        let active: Vec<String> = self.unit.programs.iter().filter(|p| p.active).map(|p| format!("  {}", p.compiled.as_ref().and_then(method_name).unwrap_or_else(|| p.name.clone()))).collect();
        let mut lines = vec![
            format!("CEE3DMP: {:<60}  {:04}-{:02}-{:02} {:02}:{:02}:{:02}", title.chars().take(60).collect::<String>().trim_end(), now.year, now.month, now.day, now.hour, now.minute, now.second),
            format!("Options: {}", options.trim_end()),
            "Active programs, in the order the run unit loaded them:".to_owned(),
        ];
        lines.extend(active);
        self.le_write(&dd, &lines, call.pos)?;
        Ok(invalid.then_some(super::DUMP_OPTIONS))
    }

    fn ceegtst(&mut self, call: &LeCall) -> Outcome {
        let heap = self.le_fullword(call, 0)?;
        let size = self.le_fullword(call, 1)?;
        let address = self.le_at(call, 2)?;
        let failed = match (heap, size) {
            (0, s) if s > 0 && s as usize <= HEAP_LIMIT => None,
            (0, s) if s > 0 => Some(super::HEAP_SHORT),
            (0, _) => Some(super::HEAP_SIZE),
            _ => Some(super::HEAP_ID),
        };
        if failed.is_none() {
            let at = self.unit.push_temporary(&vec![0; size as usize]);
            self.unit.le.heap.push((at, size as usize, false));
            self.le_store(address, &(ADDRESS_BASE + at as u32).to_be_bytes());
        }
        Ok(failed)
    }

    fn ceefrst(&mut self, call: &LeCall) -> Outcome {
        let at = self.le_at(call, 0)?;
        let b = self.le_load(at, 4, call.pos)?;
        let address = u32::from_be_bytes([b[0], b[1], b[2], b[3]]);
        let offset = address.checked_sub(ADDRESS_BASE).map(|o| o as usize);
        match self.unit.le.heap.iter_mut().find(|(start, _, freed)| Some(*start) == offset && !*freed) {
            Some(block) => {
                block.2 = true;
                Ok(None)
            }
            None => Ok(Some(super::FREE_ADDRESS)),
        }
    }

    /// Writes lines to a DD as UTF-8 text, the run's first write replacing the file, or to standard
    /// error when the DD is not given; in a CICS task, each line is an item on TD queue CESE.
    fn le_write(&mut self, dd: &str, lines: &[String], pos: Pos) -> R<()> {
        if let Some(task) = self.unit.cics.as_mut() {
            for line in lines {
                task.writeq_td(CESE, &self.page.encode(line).unwrap_or_default());
            }
            return Ok(());
        }
        let text: String = lines.iter().map(|l| format!("{l}\n")).collect();
        let Some(file) = self.unit.dds.get(dd) else {
            return self.unit.err.write_all(text.as_bytes()).map_err(|e| Abend::ironwork(format!("writing to standard error: {e}"), pos));
        };
        let first = !self.unit.le.written.iter().any(|w| w == dd);
        let opened = std::fs::OpenOptions::new().create(true).write(true).append(!first).truncate(first).open(&file.path);
        opened
            .and_then(|mut f| f.write_all(text.as_bytes()))
            .map_err(|e| Abend::ironwork(format!("DD {dd} {}: {e}", file.path.display()), pos))?;
        if first {
            self.unit.le.written.push(dd.to_owned());
        }
        Ok(())
    }
}