Skip to main content

ironwork_exec/
loader.rs

1//! The interpreter's program loader: the programs CALL can reach, read from source and compiled
2//! the first time one is called.
3
4use crate::oo::ClassCode;
5use crate::unit::{LoadError, LoadedProgram, Loader, RunUnit};
6use crate::Compiled;
7use rt::unit::FoundClass;
8use std::path::{Path, PathBuf};
9use std::rc::Rc;
10use syntax::ast::Program;
11use syntax::copy;
12
13/// Where CALL finds programs: the other programs of the first program's source, then program
14/// libraries searched by member name.
15#[derive(Clone, Debug, Default)]
16pub struct Library {
17    /// The programs not yet loaded: the first program's source's own, then those read from a
18    /// library file, which name that file as their first source.
19    pub programs: Vec<Program>,
20    pub dirs: Vec<PathBuf>,
21    pub copy: copy::Libraries,
22    pub flags: Vec<String>,
23    /// The statements whose start the run unit tells its observer of (`RunUnit::statements`).
24    pub trace_statements: Option<rt::unit::StatementFilter>,
25    /// Whether the run unit follows which bytes may hold input (`RunUnit::taint`).
26    pub trace_input: bool,
27    /// How many statements may start before the run ends with S322 (`RunUnit::statement_limit`).
28    pub statement_limit: Option<u64>,
29    /// Each PROGRAM-ID the source files of `dirs` hold, with its file, read the first time a CALL
30    /// names no member.
31    pub program_ids: Option<Vec<(String, PathBuf)>>,
32}
33
34/// The extensions of a program library's source members.
35const SOURCE_EXTENSIONS: &[&str] = &["cbl", "CBL", "cob", "COB"];
36
37impl Library {
38    /// The program `name` in the member of that name, or else in a source file whose PROGRAM-ID
39    /// it is.
40    fn search(&mut self, name: &str) -> Result<(Program, PathBuf), LoadError> {
41        let candidates = [name.to_owned(), name.to_ascii_lowercase()];
42        let member = self
43            .dirs
44            .iter()
45            .flat_map(|d| candidates.iter().flat_map(move |n| std::iter::once(String::new()).chain(SOURCE_EXTENSIONS.iter().map(|e| format!(".{e}"))).map(move |e| d.join(format!("{n}{e}")))))
46            .find(|p| p.is_file());
47        if let Some(path) = member {
48            let mut programs = self.read(name, &path)?;
49            let wanted = programs.iter().position(|p| loads_as(p, name)).or_else(|| programs.iter().position(|p| !p.is_prototype())).unwrap_or(0);
50            let found = programs.remove(wanted);
51            self.add_read(&path, programs);
52            return Ok((found, path));
53        }
54        for path in self.holding(name) {
55            let mut programs = self.read(name, &path)?;
56            if let Some(wanted) = programs.iter().position(|p| loads_as(p, name)) {
57                let found = programs.remove(wanted);
58                self.add_read(&path, programs);
59                return Ok((found, path));
60            }
61        }
62        Err(LoadError::NotFound)
63    }
64
65    fn read(&self, name: &str, path: &Path) -> Result<Vec<Program>, LoadError> {
66        let text = std::fs::read(path).map(|b| copy::decode(&b)).map_err(|e| LoadError::Compile(format!("{}: {e}", path.display())))?;
67        syntax::parse_all_with(&text, &self.copy.with_program(path)).map_err(|e| LoadError::Compile(format!("{name} does not compile: {}", e.place(&path.display().to_string()))))
68    }
69
70    /// The source files of the program libraries whose PROGRAM-ID is `name`, the directories in
71    /// order and each one's files by name (assumption C441).
72    fn holding(&mut self, name: &str) -> Vec<PathBuf> {
73        let (dirs, compliance) = (&self.dirs, self.copy.compliance());
74        let index = self.program_ids.get_or_insert_with(|| {
75            let mut index = Vec::new();
76            for dir in dirs {
77                let Ok(entries) = std::fs::read_dir(dir) else { continue };
78                let mut files: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).filter(|p| p.is_file() && p.extension().is_some_and(|e| SOURCE_EXTENSIONS.iter().any(|x| e == *x))).collect();
79                files.sort();
80                for file in files {
81                    let Ok(bytes) = std::fs::read(&file) else { continue };
82                    index.extend(syntax::program_ids(&copy::decode(&bytes), compliance).into_iter().map(|id| (id, file.clone())));
83                }
84            }
85            index
86        });
87        let mut paths: Vec<PathBuf> = index.iter().filter(|(id, _)| id.eq_ignore_ascii_case(name)).map(|(_, path)| path.clone()).collect();
88        paths.dedup();
89        paths
90    }
91
92    /// The program a CALL of `name` would load, parsed, not compiled, and kept for that CALL; None
93    /// when no program answers it or its source does not parse.
94    pub fn find(&mut self, name: &str) -> Option<&Program> {
95        if !rt::module::member_name(name) {
96            return None;
97        }
98        if !self.programs.iter().any(|p| loads_as(p, name)) {
99            let (program, path) = self.search(name).ok()?;
100            self.add_read(&path, vec![program]);
101        }
102        self.programs.iter().find(|p| loads_as(p, name))
103    }
104
105    /// Keeps programs read from the library file `path` for a later CALL.
106    pub fn add_read(&mut self, path: &Path, programs: Vec<Program>) {
107        let shown = path.display().to_string();
108        self.programs.extend(programs.into_iter().map(|mut p| {
109            if let Some(own) = p.sources.first_mut() {
110                own.clone_from(&shown);
111            }
112            p
113        }));
114    }
115}
116
117/// Whether a CALL or function invocation of `name` loads `program`: by PROGRAM-ID, or a function
118/// definition by its external name; a prototype has no code to load.
119pub(crate) fn loads_as(program: &Program, name: &str) -> bool {
120    !program.is_prototype() && program.load_name().eq_ignore_ascii_case(name)
121}
122
123impl Loader<Rc<Compiled>> for Library {
124    fn program(&mut self, name: &str) -> Result<LoadedProgram<Rc<Compiled>>, LoadError> {
125        if !rt::module::member_name(name) {
126            return Err(LoadError::NotFound);
127        }
128        let (program, source) = match self.programs.iter().position(|p| loads_as(p, name)) {
129            Some(i) => {
130                let program = self.programs.remove(i);
131                let source = program.sources.first().filter(|s| !s.is_empty()).map(PathBuf::from);
132                (program, source)
133            }
134            None => self.search(name).map(|(p, path)| (p, Some(path)))?,
135        };
136        let compiled = crate::compile(program, &self.flags).map_err(|errors| {
137            let first = syntax::most_severe(&errors).map(|e| e.place(name)).unwrap_or_default();
138            LoadError::Compile(format!("{name} does not compile: {first}"))
139        })?;
140        let compiled = Rc::new(compiled);
141        let (files, size) = Self::shape(&compiled);
142        Ok(LoadedProgram { name: compiled.program.load_name().to_ascii_uppercase(), files, size, source, compiled, recorded: Vec::new() })
143    }
144
145    fn holder(&self, entry: &str) -> Option<String> {
146        self.programs.iter().find(|p| crate::entry_points(p).iter().any(|e| e.name == entry)).map(|p| p.id.to_ascii_uppercase())
147    }
148
149    fn entry(program: &Rc<Compiled>, name: &str) -> Option<usize> {
150        program.entries.iter().position(|e| e.name == name)
151    }
152
153    fn shape(program: &Rc<Compiled>) -> (usize, usize) {
154        (program.program.files.len(), program.layout.size as usize)
155    }
156
157    fn nested(program: &Rc<Compiled>) -> &[String] {
158        &program.program.nested
159    }
160
161    fn source(program: &Rc<Compiled>, file: usize) -> Option<String> {
162        program.program.sources.get(file).cloned()
163    }
164
165    fn class(&mut self, external: &str) -> Result<Option<FoundClass<Rc<ClassCode>>>, String> {
166        let Some((program, path)) = crate::oo::find_class(self, external)? else { return Ok(None) };
167        let at = crate::compile_time().map_err(|m| format!("class {external} does not compile: {m}"))?;
168        let (code, _) = crate::oo::class_code(&program, &self.flags, at).map_err(|errors| {
169            let first = syntax::most_severe(&errors).map(|e| e.place(external)).unwrap_or_default();
170            format!("class {external} does not compile: {first}")
171        })?;
172        let mut sources = program.sources;
173        if let (Some(own), Some(path)) = (sources.first_mut(), path) {
174            *own = path;
175        }
176        Ok(Some(FoundClass { code: Rc::new(code), sources }))
177    }
178
179    fn mapset(&mut self, name: &str) -> Option<Result<rt::bms::Mapset, String>> {
180        syntax::bms::find_mapset(&self.copy, name).map(|found| found.map_err(|e| e.message))
181    }
182}
183
184/// Adding a program to the interpreter's run unit by its source's PROGRAM-ID and FILE-CONTROL.
185pub trait AddProgram {
186    fn add(&mut self, compiled: Option<Rc<Compiled>>, program: &Program, size: usize) -> usize;
187}
188
189impl AddProgram for RunUnit<'_> {
190    fn add(&mut self, compiled: Option<Rc<Compiled>>, program: &Program, size: usize) -> usize {
191        self.add_named(compiled, program.id.to_ascii_uppercase(), program.files.len(), size)
192    }
193}