Skip to main content

ironwork_rt/module/
library.rs

1//! The load modules a run has read and the directories it reads more from (load-module.md ยง8.2):
2//! a program found by the name a CALL or a function invocation gives, and a class by its external
3//! name, each as the VM holds it.
4
5use super::{DirectoryEntry, LoadedModule, read};
6use crate::bms::Mapset;
7use crate::lir::{Class, ClassPart, Program, SymId};
8use crate::oo::{ClassCode, JAVA_LANG_OBJECT, MethodCode, Part};
9use crate::unit::{FoundClass, LoadError, LoadedProgram};
10use crate::vm::Code;
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13
14/// What a program from a module must pass before the VM runs it, since a module is untrusted input.
15pub type Check = fn(&Program) -> Result<(), String>;
16
17/// Whether `name` can name a library member; any other name never reaches the filesystem.
18pub fn member_name(name: &str) -> bool {
19    !name.is_empty() && name.len() <= 30 && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '@' | '#' | '$'))
20}
21
22pub struct Modules {
23    /// Searched in order for `NAME.iwm`.
24    pub dirs: Vec<PathBuf>,
25    check: Check,
26    read: Vec<Read>,
27}
28
29/// A module the run has read: its directory, the programs no CALL has taken yet, and its mapsets.
30struct Read {
31    path: PathBuf,
32    directory: Vec<DirectoryEntry>,
33    programs: Vec<Option<Program>>,
34    mapsets: Vec<Mapset>,
35}
36
37type Found = LoadedProgram<Rc<Code>>;
38type FoundCode = FoundClass<Rc<ClassCode<Rc<Code>>>>;
39
40impl Modules {
41    pub fn new(dirs: Vec<PathBuf>, check: Check) -> Self {
42        Self { dirs, check, read: Vec::new() }
43    }
44
45    /// Registers a module read from `path`, whose programs are found by name from now on; its
46    /// number is what [`Modules::take`] takes.
47    pub fn add(&mut self, path: PathBuf, module: LoadedModule) -> usize {
48        let LoadedModule { directory, programs, mapsets } = module;
49        self.read.push(Read { path, directory, programs: programs.into_iter().map(Some).collect(), mapsets });
50        self.read.len() - 1
51    }
52
53    /// Program `ordinal` of module `k` for the run unit, checked; None once taken.
54    pub fn take(&mut self, k: usize, ordinal: usize) -> Option<Result<Found, String>> {
55        let held = self.read.get_mut(k)?;
56        let program = held.programs.get_mut(ordinal)?.take()?;
57        let entry = &held.directory[ordinal];
58        let nested = held.directory.iter().filter(|e| e.parent == Some(ordinal as u32)).map(|e| e.id.clone()).collect();
59        let name = entry.load_name().to_ascii_uppercase();
60        let checked = (self.check)(&program).map_err(|e| format!("{}: program {}: {e}", held.path.display(), entry.id));
61        Some(checked.map(|()| {
62            let code = code(program, nested, None);
63            let (files, size) = code.shape();
64            LoadedProgram { compiled: Rc::new(code), name, files, size, source: None }
65        }))
66    }
67
68    /// A program of a module already read that no CALL has taken, by the name a CALL gives: the
69    /// first such module, and in it the lowest ordinal.
70    pub fn loaded(&mut self, name: &str) -> Option<Result<Found, LoadError>> {
71        let (k, ordinal) = self.read.iter().enumerate().find_map(|(k, m)| m.untaken(|e| e.load_name().eq_ignore_ascii_case(name)).map(|o| (k, o)))?;
72        self.take(k, ordinal).map(|found| found.map_err(LoadError::Compile))
73    }
74
75    /// `NAME.iwm`, then `name.iwm`, in each directory in turn. A module found that cannot be read,
76    /// or that holds no program `NAME`, is an error, not "not found".
77    pub fn search(&mut self, name: &str) -> Result<Found, LoadError> {
78        if !member_name(name) {
79            return Err(LoadError::NotFound);
80        }
81        let Some(path) = self.file(&[name.to_owned(), name.to_ascii_lowercase()]) else { return Err(LoadError::NotFound) };
82        let module = open(&path).map_err(LoadError::Compile)?;
83        let Some(ordinal) = module.directory.iter().position(|e| e.load_name().eq_ignore_ascii_case(name)) else {
84            return Err(LoadError::Compile(format!("{}: the module holds no program {name}", path.display())));
85        };
86        let k = self.add(path, module);
87        match self.take(k, ordinal) {
88            Some(found) => found.map_err(LoadError::Compile),
89            None => Err(LoadError::NotFound),
90        }
91    }
92
93    /// The PROGRAM-ID of a program not yet taken, of a module already read, with this ENTRY name.
94    pub fn holder(&self, entry: &str) -> Option<String> {
95        self.read.iter().find_map(|m| m.untaken(|e| e.entries.iter().any(|(n, _)| n == entry)).map(|o| m.directory[o].id.to_ascii_uppercase()))
96    }
97
98    /// The COBOL class definition of this external name in a module already read, not yet taken.
99    pub fn loaded_class(&mut self, external: &str) -> Result<Option<FoundCode>, String> {
100        match self.read.iter().enumerate().find_map(|(k, m)| m.defining(external).map(|o| (k, o))) {
101            Some(found) => self.take_class(found).map(Some),
102            None => Ok(None),
103        }
104    }
105
106    /// The COBOL class definition of this external name in a module named as the source search
107    /// names the class's source (`numeric::assumptions::CLASS_SEARCH`), in each directory in turn.
108    pub fn search_class(&mut self, external: &str) -> Result<Option<FoundCode>, String> {
109        if external == JAVA_LANG_OBJECT {
110            return Ok(None);
111        }
112        let member = |n: &str| !n.is_empty() && n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
113        let simple = external.rsplit('.').next().unwrap_or(external).to_owned();
114        let mut names = vec![simple, external.replace('.', "_")];
115        names.dedup();
116        names.retain(|n| member(n));
117        let variants: Vec<String> = names.iter().flat_map(|n| [n.clone(), n.to_ascii_lowercase(), n.to_ascii_uppercase()]).collect();
118        for dir in self.dirs.clone() {
119            for variant in &variants {
120                let path = dir.join(format!("{variant}.iwm"));
121                if !path.is_file() {
122                    continue;
123                }
124                let module = open(&path)?;
125                let Some(ordinal) = module.programs.iter().position(|p| defines(p, external)) else { continue };
126                let k = self.add(path, module);
127                return self.take_class((k, ordinal)).map(Some);
128            }
129        }
130        Ok(None)
131    }
132
133    /// A mapset a module already read holds.
134    pub fn mapset(&self, name: &str) -> Option<Mapset> {
135        self.read.iter().find_map(|m| m.mapsets.iter().find(|s| s.name.eq_ignore_ascii_case(name))).cloned()
136    }
137
138    fn file(&self, names: &[String]) -> Option<PathBuf> {
139        self.dirs.iter().flat_map(|d| names.iter().map(move |n| d.join(format!("{n}.iwm")))).find(|p| p.is_file())
140    }
141
142    /// The class program at `(k, ordinal)`, checked, as the run unit's class table holds it, with
143    /// its source table.
144    fn take_class(&mut self, (k, ordinal): (usize, usize)) -> Result<FoundCode, String> {
145        let held = &mut self.read[k];
146        let Some(mut program) = held.programs[ordinal].take() else { return Err(format!("{}: program {ordinal} is taken", held.path.display())) };
147        (self.check)(&program).map_err(|e| format!("{}: class {}: {e}", held.path.display(), held.directory[ordinal].id))?;
148        let Some(class) = program.services.class.take() else { return Err(format!("{}: program {ordinal} is not a class", held.path.display())) };
149        let Class { parent, factory, object, methods, .. } = *class;
150        let sym = |id: SymId| symbol(&program, id);
151        let name = sym(program.id);
152        let part = |p: ClassPart| Part { data: Rc::new(code(p.data, Vec::new(), None)), records: p.records };
153        let methods = methods
154            .into_iter()
155            .map(|m| {
156                let method = sym(m.name);
157                MethodCode {
158                    factory: m.factory,
159                    params: m.params.iter().map(|&p| sym(p)).collect(),
160                    returns: m.returns.map(sym),
161                    own_records: usize::from(m.own_records),
162                    code: Rc::new(code(m.code, Vec::new(), Some(format!("{name}.{method}")))),
163                    name: method,
164                }
165            })
166            .collect();
167        let code = ClassCode { parent: sym(parent), factory: factory.map(part), object: object.map(part), methods };
168        let sources = program.debug.sources.iter().map(|&s| sym(s)).collect();
169        Ok(FoundClass { code: Rc::new(code), sources })
170    }
171}
172
173impl Read {
174    /// The lowest ordinal not yet taken whose directory entry `wanted` accepts.
175    fn untaken(&self, wanted: impl Fn(&DirectoryEntry) -> bool) -> Option<usize> {
176        self.directory.iter().zip(&self.programs).position(|(e, p)| p.is_some() && wanted(e))
177    }
178
179    fn defining(&self, external: &str) -> Option<usize> {
180        self.programs.iter().position(|p| p.as_ref().is_some_and(|p| defines(p, external)))
181    }
182}
183
184fn defines(program: &Program, external: &str) -> bool {
185    program.services.class.as_ref().is_some_and(|c| symbol(program, c.external) == external)
186}
187
188fn symbol(program: &Program, id: SymId) -> String {
189    program.symbols.get(id as usize).cloned().unwrap_or_default()
190}
191
192/// A program from a module as the VM holds it, with the PROGRAM-IDs of the programs it directly
193/// contains and, for a method, the `Class.method` a dump lists it by.
194fn code(program: Program, nested: Vec<String>, method: Option<String>) -> Code {
195    let entries = program.services.entries.iter().map(|e| symbol(&program, e.name)).collect();
196    let (files, size) = (program.services.files.len(), program.storage.size as usize);
197    Code::new(Ok(program), entries, files, size, nested, method)
198}
199
200/// The module at `path`, read and checked, or why not with the path in front.
201fn open(path: &Path) -> Result<LoadedModule, String> {
202    let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
203    read(&bytes).map_err(|e| format!("{}: {e}", path.display()))
204}