use crate::abend::Abend;
use crate::files::{Dds, Open};
use crate::oo::ClassCode;
use crate::storage::Loc;
use crate::taint::Taint;
use crate::vocab::{OpenMode, Pos};
use numeric::Dialect;
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub const ADDRESS_BASE: u32 = 0x0001_0000;
pub const RETURN_CODE: usize = 0;
const RESERVED: usize = 8;
const ALIGNMENT: usize = 8;
pub struct Loaded<H> {
pub compiled: Option<H>,
pub name: String,
pub base: usize,
pub size: usize,
pub placed: bool,
pub files: Vec<Option<Open>>,
pub locked: Vec<bool>,
pub initialized: bool,
pub active: bool,
pub dynamic: bool,
pub entry: Option<usize>,
pub altered: Vec<Option<usize>>,
pub source: Option<PathBuf>,
}
impl<H> Loaded<H> {
pub fn new(compiled: Option<H>, name: String, base: usize, size: usize, files: usize) -> Self {
let (locked, files) = (vec![false; files], (0..files).map(|_| None).collect());
Self { compiled, name, base, size, placed: true, files, locked, initialized: false, active: false, dynamic: false, entry: None, altered: Vec::new(), source: None }
}
fn set_aside(&mut self) -> Held {
let files = self.files.iter_mut().map(Option::take).collect();
let locked = std::mem::replace(&mut self.locked, vec![false; self.files.len()]);
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) };
(self.placed, self.initialized, self.active, self.dynamic) = (false, false, false, false);
held
}
fn restore(&mut self, held: Held) {
(self.base, self.placed, self.files, self.locked, self.altered) = (held.base, held.placed, held.files, held.locked, held.altered);
(self.initialized, self.active, self.dynamic) = (held.initialized, held.active, held.dynamic);
}
}
struct Enclave {
programs: Vec<Held>,
externals: Externals,
connectors: HashMap<(usize, usize), Connector>,
heap: Vec<(usize, usize, bool)>,
random: Option<u32>,
return_code: ([u8; 2], bool),
}
struct Held {
base: usize,
placed: bool,
files: Vec<Option<Open>>,
locked: Vec<bool>,
initialized: bool,
active: bool,
dynamic: bool,
altered: Vec<Option<usize>>,
}
pub enum LoadError {
NotFound,
Compile(String),
}
pub struct LoadedProgram<H> {
pub compiled: H,
pub name: String,
pub files: usize,
pub size: usize,
pub source: Option<PathBuf>,
pub recorded: Vec<(String, Option<crate::module::SourceFile>)>,
}
pub struct FoundClass<C> {
pub code: C,
pub sources: Vec<String>,
}
pub trait Loader<H> {
fn program(&mut self, name: &str) -> Result<LoadedProgram<H>, LoadError>;
fn holder(&self, entry: &str) -> Option<String>;
fn entry(program: &H, name: &str) -> Option<usize>;
fn shape(program: &H) -> (usize, usize);
fn nested(program: &H) -> &[String];
fn source(program: &H, file: usize) -> Option<String>;
fn class(&mut self, external: &str) -> Result<Option<FoundClass<Rc<ClassCode<H>>>>, String>;
fn mapset(&mut self, name: &str) -> Option<Result<crate::bms::Mapset, String>>;
}
pub trait UnitHost<'w> {
type Program: Clone;
type Loader: Loader<Self::Program>;
fn unit(&mut self) -> &mut RunUnit<'w, Self::Program, Self::Loader>;
}
#[derive(Clone, Copy, Debug)]
pub enum Clock {
System,
Fixed(i64, u32),
}
pub enum Event<'a> {
Open { dd: &'a str, mode: OpenMode, path: &'a Path },
Close { dd: &'a str, path: &'a Path },
Load { program: &'a str, source: Option<&'a Path>, recorded: &'a [(String, Option<crate::module::SourceFile>)] },
Paragraph { program: &'a str, name: &'a str, index: usize },
Sink { kind: &'static str, file: &'a str, line: u32, operand: &'a str, input: Option<bool> },
Statement { file: &'a str, line: u32 },
}
pub const SINK_KINDS: [&str; 16] = [
"cics-dynamic-transfer",
"cics-sysid",
"connection-target",
"dynamic-file-path",
"dynamic-program-load",
"dynamic-sql",
"http-header",
"log",
"os-command",
"outbound-host",
"outbound-http",
"queue-name",
"record-key",
"record-update",
"screen",
"web-response",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StatementFilter {
All,
Lines(HashSet<u32>),
}
pub type Observer<'w> = Box<dyn FnMut(Event<'_>) + 'w>;
pub const MAX_DEPTH: usize = 100;
#[derive(Default)]
pub struct Externals {
storage: HashMap<(bool, String), (usize, usize)>,
files: Vec<Option<Open>>,
locked: Vec<bool>,
file_names: HashMap<String, usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Connector {
External(usize),
Program(usize, usize),
}
pub const OS_COMMAND_ROUTINES: &[&str] = &["SYSTEM", "C$SYSTEM", "CBL_EXEC_RUN_UNIT", "CBL_GC_HOSTED", "BXPSYSTM"];
pub struct RunUnit<'w, H, L: Loader<H>> {
pub mem: Vec<u8>,
pub depth: usize,
pub programs: Vec<Loaded<H>>,
names: HashMap<String, usize>,
pub library: L,
pub dds: Dds,
pub sysin: Option<Box<dyn BufRead + 'w>>,
pub clock: Clock,
pub out: &'w mut dyn Write,
pub err: &'w mut dyn Write,
pub cics: Option<crate::cics::Task>,
pub eib: usize,
pub cics_files: HashMap<String, Open>,
pub sql: Option<crate::sql::Session<'w>>,
pub le: crate::le::State,
pub oo: crate::oo::Objects<Rc<ClassCode<H>>>,
pub observer: Option<Observer<'w>>,
pub random: Option<u32>,
externals: Externals,
connectors: HashMap<(usize, usize), Connector>,
pub entries: Vec<crate::set::Entry>,
pub statements: Option<StatementFilter>,
pub taint: Option<Taint>,
pub statement_limit: Option<u64>,
recent: VecDeque<Started>,
overrun: Option<Overrun>,
pub(crate) sysin_ended: HashSet<(u16, u32, u32)>,
set_aside: Vec<Enclave>,
}
fn end_file(f: Open, unclosed: bool) -> std::io::Result<()> {
if unclosed { f.abandon() } else { f.close() }
}
const LOOP_WINDOW: usize = 4096;
#[derive(Clone, Copy, PartialEq, Eq)]
struct Started {
program: usize,
depth: usize,
pos: Pos,
}
struct Overrun {
head: Started,
lines: Vec<u32>,
grace: usize,
}
fn loop_of(recent: &VecDeque<Started>, now: Started) -> Overrun {
let key = |s: &Started| (s.program, s.depth, s.pos.file, s.pos.line, s.pos.col);
let mut seen: HashMap<_, usize> = HashMap::new();
for s in recent {
*seen.entry(key(s)).or_default() += 1;
}
let recurring: Vec<&Started> = recent.iter().filter(|s| seen[&key(s)] > 1).collect();
let Some(outer) = recurring.iter().map(|s| s.depth).min() else { return Overrun { head: now, lines: Vec::new(), grace: 0 } };
let program = recurring.iter().rev().find(|s| s.depth == outer).map_or(now.program, |s| s.program);
let frame: Vec<&Started> = recurring.into_iter().filter(|s| s.depth == outer && s.program == program).collect();
let file = frame.iter().map(|s| s.pos.file).min().unwrap_or(now.pos.file);
let head = frame.iter().filter(|s| s.pos.file == file).min_by_key(|s| (s.pos.line, s.pos.col)).map_or(now, |s| **s);
let mut lines: Vec<u32> = frame.iter().filter(|s| s.pos.file == file).map(|s| s.pos.line).collect();
lines.sort_unstable();
lines.dedup();
Overrun { head, lines, grace: LOOP_WINDOW }
}
impl<H, L: Loader<H>> RunUnit<'_, H, L> {
pub fn write(&mut self, offset: usize, bytes: &[u8]) {
self.mem[offset..offset + bytes.len()].copy_from_slice(bytes);
self.mark(offset, bytes.len());
}
pub fn write_input(&mut self, offset: usize, bytes: &[u8]) {
self.mem[offset..offset + bytes.len()].copy_from_slice(bytes);
self.mark_input(offset, bytes.len(), true);
}
pub fn mark(&mut self, offset: usize, len: usize) {
if let Some(t) = self.taint.as_mut() {
let pending = t.pending();
t.set(offset, len, pending);
}
}
pub fn mark_input(&mut self, offset: usize, len: usize, input: bool) {
if let Some(t) = self.taint.as_mut() {
t.set(offset, len, input);
}
}
pub fn taint_read(&mut self, loc: Loc) {
if let Some(t) = self.taint.as_mut() {
t.read(loc.offset, loc.len);
}
}
pub fn writing(&mut self, on: bool) -> bool {
self.taint.as_mut().is_some_and(|t| t.writing(on))
}
pub fn statement_starts(&mut self) {
if let Some(t) = self.taint.as_mut() {
t.start_statement();
}
}
pub fn take_input(&mut self) {
if let Some(t) = self.taint.as_mut() {
t.take_input();
}
}
pub fn holds_input(&self, offset: usize, len: usize) -> bool {
self.taint.as_ref().is_some_and(|t| t.any(offset, len))
}
pub fn pending(&self) -> bool {
self.taint.as_ref().is_some_and(Taint::pending)
}
pub fn resume_statement(&mut self, read_before: bool) {
if let Some(t) = self.taint.as_mut() {
t.resume_statement(read_before);
}
}
pub fn unfollowed(&mut self, what: &'static str) {
if let Some(t) = self.taint.as_mut() {
t.unfollowed(what);
}
}
pub fn input_at_sink(&self) -> Option<bool> {
self.taint.as_ref().and_then(Taint::at_sink)
}
}
impl<'w, H: Clone, L: Loader<H>> RunUnit<'w, H, L> {
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 {
Self {
mem: vec![0; RESERVED],
depth: 0,
programs: Vec::new(),
names: HashMap::new(),
library,
dds,
sysin,
clock,
out,
err,
cics: None,
eib: 0,
cics_files: HashMap::new(),
sql: None,
le: crate::le::State::default(),
oo: Default::default(),
observer: None,
random: None,
externals: Externals::default(),
connectors: HashMap::new(),
entries: Vec::new(),
statements: None,
taint: None,
statement_limit: None,
recent: VecDeque::new(),
overrun: None,
sysin_ended: HashSet::new(),
set_aside: Vec::new(),
}
}
fn allocate(&mut self, size: usize) -> usize {
let base = self.mem.len().div_ceil(ALIGNMENT) * ALIGNMENT;
self.mem.resize(base + size, 0);
base
}
pub fn add_named(&mut self, compiled: Option<H>, name: String, files: usize, size: usize) -> usize {
let base = self.allocate(size);
let index = self.programs.len();
self.names.insert(name.clone(), index);
self.programs.push(Loaded::new(compiled, name, base, size, files));
index
}
pub fn begin_cics_run_unit(&mut self) {
let programs = self.programs.iter_mut().map(Loaded::set_aside).collect();
let (externals, connectors) = (std::mem::take(&mut self.externals), std::mem::take(&mut self.connectors));
let return_code = ([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]], self.holds_input(RETURN_CODE, 2));
self.mem[RETURN_CODE..RETURN_CODE + 2].fill(0);
self.mark_input(RETURN_CODE, 2, false);
let (heap, random) = (std::mem::take(&mut self.le.heap), self.random.take());
self.set_aside.push(Enclave { programs, externals, connectors, heap, random, return_code });
}
pub fn end_cics_run_unit(&mut self, xctl: bool) -> Result<(), String> {
let mut closed = Ok(());
for program in &mut self.programs {
for f in program.files.iter_mut().filter_map(Option::take) {
if let Err(e) = f.close() {
closed = closed.and(Err(format!("closing a file of {}: {e}", program.name)));
}
}
drop(program.set_aside());
}
closed = closed.and(self.close_external_files(false));
let Some(enclave) = self.set_aside.pop() else { return closed };
for (program, held) in self.programs.iter_mut().zip(enclave.programs) {
program.restore(held);
}
(self.externals, self.connectors, self.le.heap, self.random) = (enclave.externals, enclave.connectors, enclave.heap, enclave.random);
if !xctl {
let (bytes, input) = enclave.return_code;
self.mem[RETURN_CODE..RETURN_CODE + 2].copy_from_slice(&bytes);
self.mark_input(RETURN_CODE, 2, input);
}
closed
}
pub fn load_entry(&mut self, name: &str, copy: bool) -> Result<(usize, Option<usize>), LoadError> {
if let Some(i) = self.find(name) {
return Ok((i, self.programs[i].entry));
}
let name = name.to_ascii_uppercase();
let index = match self.programs.iter().position(|p| p.compiled.as_ref().is_some_and(|c| L::entry(c, &name).is_some())) {
Some(i) => i,
None => {
let holder = self.library.holder(&name);
self.load(holder.as_deref().unwrap_or(&name))?
}
};
let Some(compiled) = self.programs[index].compiled.clone() else { return Ok((index, None)) };
let Some(entry) = L::entry(&compiled, &name) else { return Ok((index, None)) };
if !copy {
return Ok((index, Some(entry)));
}
let (files, size) = L::shape(&compiled);
let copy = self.add_named(Some(compiled), name, files, size);
self.programs[copy].entry = Some(entry);
self.programs[copy].source = self.programs[index].source.clone();
Ok((copy, Some(entry)))
}
pub fn push_temporary(&mut self, bytes: &[u8]) -> usize {
let at = self.allocate(bytes.len());
self.write(at, bytes);
at
}
pub fn release_temporaries(&mut self, mark: usize) {
let external = self.externals.storage.values().all(|&(at, _)| at < mark);
if self.programs.iter().all(|p| !p.placed || p.base + p.size <= mark) && self.le.heap_end() <= mark && external {
self.mem.truncate(mark.max(RESERVED));
if let Some(t) = self.taint.as_mut() {
t.truncate(self.mem.len());
}
}
}
pub fn enter(&mut self, pos: Pos) -> Result<(), Abend> {
if self.depth >= MAX_DEPTH {
return Err(Abend::ironwork(format!("PERFORM and CALL nest deeper than {MAX_DEPTH}"), pos));
}
self.depth += 1;
Ok(())
}
pub fn activate(&mut self, me: usize, initial: bool) -> (usize, bool) {
if !self.programs[me].placed {
let base = self.allocate(self.programs[me].size);
(self.programs[me].base, self.programs[me].placed) = (base, true);
}
let program = &mut self.programs[me];
program.active = true;
(program.base, !program.initialized || initial)
}
pub fn initialized(&mut self, me: usize) {
self.programs[me].initialized = true;
self.programs[me].altered.clear();
}
pub fn find(&self, name: &str) -> Option<usize> {
if name.bytes().any(|b| b.is_ascii_lowercase()) {
return self.names.get(&name.to_ascii_uppercase()).copied();
}
self.names.get(name).copied()
}
pub fn load(&mut self, name: &str) -> Result<usize, LoadError> {
let name = name.to_ascii_uppercase();
if let Some(i) = self.find(&name) {
return Ok(i);
}
let loaded = self.library.program(&name)?;
self.notify(Event::Load { program: &name, source: loaded.source.as_deref(), recorded: &loaded.recorded });
let index = self.add_named(Some(loaded.compiled), loaded.name, loaded.files, loaded.size);
self.programs[index].source = loaded.source;
Ok(index)
}
pub const fn observed(&self) -> bool {
self.observer.is_some()
}
pub fn start_statement(&mut self, program: usize, pos: Pos) -> Result<(), Abend> {
let Some(left) = self.statement_limit.as_mut() else { return Ok(()) };
let now = Started { program, depth: self.depth, pos };
if *left > 0 {
*left -= 1;
if self.recent.len() == LOOP_WINDOW {
self.recent.pop_front();
}
self.recent.push_back(now);
return Ok(());
}
let overrun = self.overrun.get_or_insert_with(|| loop_of(&self.recent, now));
let message = "the run reached its statement limit, as a step past its TIME= ends";
if now == overrun.head && !overrun.lines.is_empty() {
const SHOWN: usize = 24;
let mut lines = overrun.lines.iter().take(SHOWN).map(u32::to_string).collect::<Vec<_>>().join(", ");
if overrun.lines.len() > SHOWN {
lines += &format!(" and {} more", overrun.lines.len() - SHOWN);
}
return Err(Abend { code: crate::abend::AbendCode::TimeLimit, message: format!("{message}, in the loop over lines {lines}"), pos, file: None });
}
if overrun.grace == 0 {
return Err(Abend { code: crate::abend::AbendCode::TimeLimit, message: message.into(), pos, file: None });
}
overrun.grace -= 1;
Ok(())
}
pub fn traces(&self, line: u32) -> bool {
match &self.statements {
None => false,
Some(StatementFilter::All) => self.observer.is_some(),
Some(StatementFilter::Lines(lines)) => self.observer.is_some() && lines.contains(&line),
}
}
pub fn notify(&mut self, event: Event<'_>) {
if let Event::Sink { kind, .. } = &event {
debug_assert!(SINK_KINDS.contains(kind), "the sink kind {kind} is not in rt::unit::SINK_KINDS");
}
if let Some(observer) = self.observer.as_mut() {
observer(event);
}
}
pub fn close_all(&mut self, unclosed: bool) -> Result<(), String> {
for program in &mut self.programs {
for f in program.files.iter_mut().filter_map(Option::take) {
end_file(f, unclosed).map_err(|e| format!("closing a file of {}: {e}", program.name))?;
}
}
self.close_external_files(unclosed)
}
fn close_external_files(&mut self, unclosed: bool) -> Result<(), String> {
let mut closed = Ok(());
for (name, &k) in &self.externals.file_names {
if let Some(f) = self.externals.files[k].take()
&& let Err(e) = end_file(f, unclosed)
{
closed = closed.and(Err(format!("closing EXTERNAL file {name}: {e}")));
}
}
closed
}
pub fn external(&mut self, name: &str, file: bool, size: usize, dialect: Dialect) -> Result<usize, String> {
let key = (file, name.to_owned());
if let Some(&(at, had)) = self.externals.storage.get(&key) {
return if had == size {
Ok(at)
} else if size < had && !file && dialect == Dialect::Gnucobol {
let _ = writeln!(self.err, "ironwork: EXTERNAL record {name} has {had} bytes in the run unit, and this program describes {size}");
Ok(at)
} else {
let what = if file { "the record area of EXTERNAL file" } else { "EXTERNAL record" };
Err(format!("{what} {name} has {had} bytes in the run unit, and this program describes {size}"))
};
}
let at = self.allocate(size);
self.externals.storage.insert(key, (at, size));
Ok(at)
}
pub fn external_file(&mut self, name: &str) -> Connector {
let (files, locked) = (&mut self.externals.files, &mut self.externals.locked);
let k = *self.externals.file_names.entry(name.to_owned()).or_insert_with(|| {
files.push(None);
locked.push(false);
files.len() - 1
});
Connector::External(k)
}
pub fn connect(&mut self, me: usize, k: usize, to: Connector) {
self.connectors.insert((me, k), to);
}
fn connector(&self, mut me: usize, mut k: usize) -> Option<Connector> {
let mut to = None;
while let Some(&c) = self.connectors.get(&(me, k)) {
to = Some(c);
match c {
Connector::External(_) => break,
Connector::Program(p, j) => (me, k) = (p, j),
}
}
to
}
pub fn file(&mut self, me: usize, k: usize) -> &mut Option<Open> {
match self.connector(me, k) {
None => &mut self.programs[me].files[k],
Some(Connector::External(e)) => &mut self.externals.files[e],
Some(Connector::Program(p, j)) => &mut self.programs[p].files[j],
}
}
pub fn locked(&mut self, me: usize, k: usize) -> &mut bool {
match self.connector(me, k) {
None => &mut self.programs[me].locked[k],
Some(Connector::External(e)) => &mut self.externals.locked[e],
Some(Connector::Program(p, j)) => &mut self.programs[p].locked[j],
}
}
pub fn file_ref(&self, me: usize, k: usize) -> &Option<Open> {
match self.connector(me, k) {
None => &self.programs[me].files[k],
Some(Connector::External(e)) => &self.externals.files[e],
Some(Connector::Program(p, j)) => &self.programs[p].files[j],
}
}
pub fn return_code(&self) -> i16 {
i16::from_be_bytes([self.mem[RETURN_CODE], self.mem[RETURN_CODE + 1]])
}
pub fn now(&self) -> (i64, u32) {
match self.clock {
Clock::Fixed(s, h) => (s, h),
Clock::System => {
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
(d.as_secs() as i64, d.subsec_millis() / 10)
}
}
}
}