Skip to main content

ironwork_rt/module/
programs.rs

1//! Writing and reading the programs of a module: the sections that split a `lir::Program` between them.
2
3use super::codec::{Decode, Encode, Reader, Writer};
4use super::{Module, ModuleError, ModuleWriter, Section, StringTable};
5use crate::bms::Mapset;
6use crate::codec_struct;
7use crate::lir::{AssignItem, 
8    AbendText, Block, Code, Cond, Const, Debug, Edit, Expr, Item, ParaId, Paragraph, Place, Plans, Program, ProgramOptions, Range,
9    Services, SqlEntry, Storage, SymId,
10};
11
12/// A program's line in the `DIRECTORY` section (load-module.md §6).
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct DirectoryEntry {
15    /// PROGRAM-ID exactly as written.
16    pub id: String,
17    /// A user-defined function's external name, which an invocation loads it by; None for a
18    /// program, which a CALL loads by `id`.
19    pub external: Option<String>,
20    /// The ordinal of the containing program.
21    pub parent: Option<u32>,
22    pub common: bool,
23    /// ENTRY names and the paragraphs they enter.
24    pub entries: Vec<(String, ParaId)>,
25    /// USING: true for BY VALUE, in order.
26    pub params: Vec<bool>,
27    pub returning: bool,
28    /// Visible to a dynamic CALL.
29    pub dynamic: bool,
30}
31
32codec_struct!(DirectoryEntry { id, external, parent, common, entries, params, returning, dynamic });
33
34impl DirectoryEntry {
35    /// The entry for a top-level program that no parse of nesting or ENTRY has refined.
36    pub fn top_level(program: &Program) -> Self {
37        Self {
38            id: program.symbols.get(program.id as usize).cloned().unwrap_or_default(),
39            external: None,
40            parent: None,
41            common: false,
42            entries: Vec::new(),
43            params: vec![false; program.storage.using.len()],
44            returning: program.storage.returning.is_some(),
45            dynamic: true,
46        }
47    }
48
49    /// The name a CALL or a function invocation finds it by.
50    pub fn load_name(&self) -> &str {
51        self.external.as_deref().unwrap_or(&self.id)
52    }
53}
54
55/// A file the compile read, as the run journal of a run of its source names it (load-module.md
56/// §9.2): the library it was found in, 0 the source's own directory and then each `-I` in order,
57/// its path from there, and its SHA-256 and length.
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct SourceFile {
60    pub root: u32,
61    pub path: String,
62    pub sha256: [u8; 32],
63    pub bytes: u64,
64}
65
66codec_struct!(SourceFile { root, path, sha256, bytes } check source_file_valid);
67
68/// A path relative to its library, with `/` between its parts, so no record of one names a place
69/// outside it.
70fn source_file_valid(file: &SourceFile) -> Result<(), String> {
71    let relative = !file.path.contains('\\') && file.path.split('/').all(|part| !matches!(part, "" | "." | ".."));
72    if relative { Ok(()) } else { Err(format!("source file {:?} is not a path within its library", file.path)) }
73}
74
75/// The programs of a module, in ordinal order, with their directory, the mapsets they use, and
76/// for each program the file each source of its debug table names, None where the compiler
77/// supplied the member or no file was recorded.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct LoadedModule {
80    pub directory: Vec<DirectoryEntry>,
81    pub programs: Vec<Program>,
82    pub mapsets: Vec<Mapset>,
83    pub files: Vec<Vec<Option<SourceFile>>>,
84}
85
86/// Every field of a `Program`, listed once so a new field is a compile error here.
87struct Parts<'a> {
88    id: &'a SymId,
89    options: &'a ProgramOptions,
90    initial: &'a bool,
91    recursive: &'a bool,
92    storage: &'a Storage,
93    items: &'a Vec<Item>,
94    paragraphs: &'a Vec<Paragraph>,
95    procedure_start: &'a ParaId,
96    ranges: &'a Vec<Range>,
97    blocks: &'a Vec<Block>,
98    places: &'a Vec<Place>,
99    exprs: &'a Vec<Expr>,
100    conds: &'a Vec<Cond>,
101    consts: &'a Vec<Const>,
102    plans: &'a Plans,
103    services: &'a Services,
104    sql: &'a Vec<SqlEntry>,
105    abends: &'a Vec<AbendText>,
106    edits: &'a Vec<Edit>,
107    symbols: &'a Vec<String>,
108    debug: &'a Debug,
109}
110
111impl<'a> Parts<'a> {
112    fn of(program: &'a Program) -> Self {
113        let Program {
114            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places,
115            exprs, conds, consts, plans, services, sql, abends, edits, symbols, debug,
116        } = program;
117        Self {
118            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places,
119            exprs, conds, consts, plans, services, sql, abends, edits, symbols, debug,
120        }
121    }
122
123    fn encode_lir(&self, w: &mut Writer) {
124        self.id.encode(w);
125        self.initial.encode(w);
126        self.recursive.encode(w);
127        self.paragraphs.encode(w);
128        self.procedure_start.encode(w);
129        self.ranges.encode(w);
130        self.blocks.encode(w);
131        self.places.encode(w);
132        self.exprs.encode(w);
133        self.conds.encode(w);
134        self.consts.encode(w);
135        self.plans.encode(w);
136        self.services.encode(w);
137        self.abends.encode(w);
138        self.symbols.encode(w);
139    }
140}
141
142/// One program's record in the `LAYOUT` section.
143pub type LayoutRecord = (Storage, Vec<Item>, Vec<Edit>);
144
145/// One program's record in the `LIR` section: the fields of `Program` no other section holds.
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct LirRecord {
148    pub id: SymId,
149    pub initial: bool,
150    pub recursive: bool,
151    pub paragraphs: Vec<Paragraph>,
152    pub procedure_start: ParaId,
153    pub ranges: Vec<Range>,
154    pub blocks: Vec<Block>,
155    pub places: Vec<Place>,
156    pub exprs: Vec<Expr>,
157    pub conds: Vec<Cond>,
158    pub consts: Vec<Const>,
159    pub plans: Plans,
160    pub services: Services,
161    pub abends: Vec<AbendText>,
162    pub symbols: Vec<String>,
163}
164
165impl LirRecord {
166    pub fn code(&self) -> Code<'_> {
167        Code {
168            id: self.id,
169            initial: self.initial,
170            recursive: self.recursive,
171            paragraphs: &self.paragraphs,
172            procedure_start: self.procedure_start,
173            ranges: &self.ranges,
174            blocks: &self.blocks,
175            places: &self.places,
176            exprs: &self.exprs,
177            conds: &self.conds,
178            consts: &self.consts,
179            plans: &self.plans,
180            services: &self.services,
181            abends: &self.abends,
182            symbols: &self.symbols,
183        }
184    }
185}
186
187impl Decode for LirRecord {
188    fn decode(r: &mut Reader<'_>) -> Result<Self, ModuleError> {
189        Ok(Self {
190            id: Decode::decode(r)?,
191            initial: Decode::decode(r)?,
192            recursive: Decode::decode(r)?,
193            paragraphs: Decode::decode(r)?,
194            procedure_start: Decode::decode(r)?,
195            ranges: Decode::decode(r)?,
196            blocks: Decode::decode(r)?,
197            places: Decode::decode(r)?,
198            exprs: Decode::decode(r)?,
199            conds: Decode::decode(r)?,
200            consts: Decode::decode(r)?,
201            plans: Decode::decode(r)?,
202            services: Decode::decode(r)?,
203            abends: Decode::decode(r)?,
204            symbols: Decode::decode(r)?,
205        })
206    }
207}
208
209/// Program count, then one record per program.
210fn per_program(w: &mut Writer, programs: &[Program], record: impl Fn(&Parts<'_>, &mut Writer)) {
211    w.count(programs.len());
212    for program in programs {
213        record(&Parts::of(program), w);
214    }
215}
216
217fn encode_module(programs: &[Program], directory: &[DirectoryEntry], mapsets: &[Mapset], files: &[Vec<Option<SourceFile>>]) -> Vec<u8> {
218    let mut m = ModuleWriter::new();
219    m.section(Section::DIRECTORY, |w| {
220        w.count(directory.len());
221        for entry in directory {
222            entry.encode(w);
223        }
224    });
225    m.section(Section::OPTIONS, |w| per_program(w, programs, |p, w| p.options.encode(w)));
226    m.section(Section::LAYOUT, |w| {
227        per_program(w, programs, |p, w| {
228            p.storage.encode(w);
229            p.items.encode(w);
230            p.edits.encode(w);
231        });
232    });
233    m.section(Section::LIR, |w| {
234        per_program(w, programs, |p, w| p.encode_lir(w));
235        let assigned = assign_items(programs);
236        if !assigned.is_empty() {
237            assigned.encode(w);
238        }
239    });
240    m.section(Section::SQL, |w| per_program(w, programs, |p, w| p.sql.encode(w)));
241    m.section(Section::BMS, |w| {
242        w.count(mapsets.len());
243        for mapset in mapsets {
244            mapset.encode(w);
245        }
246    });
247    m.section(Section::DEBUG, |w| {
248        w.count(programs.len());
249        for (program, files) in programs.iter().zip(files) {
250            Parts::of(program).debug.encode(w);
251            files.encode(w);
252        }
253    });
254    m.finish()
255}
256
257/// Each file that takes its name from a data item, as (program, file, item): the LIR section's
258/// last field, written only when there is one, so a module without one keeps 0.5's shape
259/// (load-module.md §3.4).
260fn assign_items(programs: &[Program]) -> Vec<(u32, u32, AssignItem)> {
261    let mut out = Vec::new();
262    for (n, program) in programs.iter().enumerate() {
263        for (k, file) in program.services.files.iter().enumerate() {
264            if let Some(item) = file.assign_item {
265                out.push((n as u32, k as u32, item));
266            }
267        }
268    }
269    out
270}
271
272/// The LIR section: a record per program, then the files' data items [`assign_items`] wrote.
273fn lir_records(module: &Module<'_>, strings: &StringTable, expected: usize) -> Result<Vec<LirRecord>, ModuleError> {
274    let mut r = module.reader(Section::LIR, strings)?;
275    let at = r.position();
276    let count = r.count()?;
277    if count != expected {
278        return Err(r.malformed(at, format!("{count} records for {expected} programs")));
279    }
280    let mut bodies = Vec::with_capacity(count);
281    for _ in 0..count {
282        bodies.push(LirRecord::decode(&mut r)?);
283    }
284    if r.remaining() > 0 {
285        let at = r.position();
286        for (n, k, item) in Vec::<(u32, u32, AssignItem)>::decode(&mut r)? {
287            let body = bodies.get_mut(n as usize).ok_or_else(|| r.malformed(at, format!("an assign item for program {n} of {count}")))?;
288            let places = body.places.len();
289            let file = body.services.files.get_mut(k as usize).ok_or_else(|| r.malformed(at, format!("an assign item for file {k} of program {n}")))?;
290            if item.place as usize >= places {
291                return Err(r.malformed(at, format!("an assign item's place {} of {places}", item.place)));
292            }
293            file.assign_item = Some(item);
294        }
295    }
296    r.finish()?;
297    Ok(bodies)
298}
299
300/// For each program, no file recorded for any source of its debug table.
301fn unrecorded(programs: &[Program]) -> Vec<Vec<Option<SourceFile>>> {
302    programs.iter().map(|p| vec![None; p.debug.sources.len()]).collect()
303}
304
305/// A module of `programs`, each a top-level program in the directory, with no mapsets and no files
306/// recorded. Same input, same bytes.
307pub fn write(programs: &[Program]) -> Vec<u8> {
308    let directory: Vec<_> = programs.iter().map(DirectoryEntry::top_level).collect();
309    encode_module(programs, &directory, &[], &unrecorded(programs))
310}
311
312/// A module with the caller's directory and mapsets and no files recorded, refused (as the reader
313/// would) if either, or a program, is invalid.
314pub fn write_with(programs: &[Program], directory: &[DirectoryEntry], mapsets: &[Mapset]) -> Result<Vec<u8>, ModuleError> {
315    let files = unrecorded(programs);
316    write_module(&LoadedModule { directory: directory.to_vec(), programs: programs.to_vec(), mapsets: mapsets.to_vec(), files })
317}
318
319/// The module `module` describes, refused (as the reader would) if its directory, mapsets, a
320/// program or a program's files are invalid.
321pub fn write_module(module: &LoadedModule) -> Result<Vec<u8>, ModuleError> {
322    let LoadedModule { directory, programs, mapsets, files } = module;
323    check_directory(directory, programs)?;
324    for program in programs {
325        crate::lir::program_valid(program).map_err(|reason| bad("LIR", reason))?;
326    }
327    check_mapsets(mapsets).map_err(|reason| bad(Section::BMS.name, reason))?;
328    check_files(files, programs).map_err(|reason| bad(Section::DEBUG.name, reason))?;
329    Ok(encode_module(programs, directory, mapsets, files))
330}
331
332/// One file, or none, for each source of each program's debug table.
333fn check_files(files: &[Vec<Option<SourceFile>>], programs: &[Program]) -> Result<(), String> {
334    if files.len() != programs.len() {
335        return Err(format!("files for {} programs of {}", files.len(), programs.len()));
336    }
337    for (ordinal, (files, program)) in files.iter().zip(programs).enumerate() {
338        if files.len() != program.debug.sources.len() {
339            return Err(format!("program {ordinal} records {} files for {} sources", files.len(), program.debug.sources.len()));
340        }
341        files.iter().flatten().try_for_each(source_file_valid)?;
342    }
343    Ok(())
344}
345
346/// Mapsets are held once each, in ascending order of name (load-module.md §5.3).
347fn check_mapsets(mapsets: &[Mapset]) -> Result<(), String> {
348    match mapsets.windows(2).find(|pair| pair[0].name >= pair[1].name) {
349        Some(pair) => Err(format!("mapset {} follows mapset {}", pair[1].name, pair[0].name)),
350        None => Ok(()),
351    }
352}
353
354fn bad(section: &'static str, reason: impl Into<String>) -> ModuleError {
355    ModuleError::Malformed { section, offset: 0, reason: reason.into() }
356}
357
358fn check_directory(directory: &[DirectoryEntry], programs: &[Program]) -> Result<(), ModuleError> {
359    let name = Section::DIRECTORY.name;
360    if directory.len() != programs.len() {
361        return Err(bad(name, format!("{} entries for {} programs", directory.len(), programs.len())));
362    }
363    for (ordinal, (entry, program)) in directory.iter().zip(programs).enumerate() {
364        let symbol = program.symbols.get(program.id as usize);
365        if symbol != Some(&entry.id) {
366            return Err(bad(name, format!("program {ordinal} is {symbol:?} in its symbols, {:?} in the directory", entry.id)));
367        }
368        if entry.parent.is_some_and(|p| p as usize >= ordinal) {
369            return Err(bad(name, format!("program {ordinal} has parent {}, which does not precede it", entry.parent.unwrap_or(0))));
370        }
371        if let Some((entry_name, _)) = entry.entries.iter().find(|(_, para)| *para as usize >= program.paragraphs.len()) {
372            return Err(bad(name, format!("program {ordinal} ENTRY {entry_name} names a paragraph it lacks")));
373        }
374    }
375    Ok(())
376}
377
378/// A section of a count and that many records, the count being the directory's.
379fn records<T: Decode>(module: &Module<'_>, strings: &StringTable, section: Section, expected: usize) -> Result<Vec<T>, ModuleError> {
380    let mut r = module.reader(section, strings)?;
381    let at = r.position();
382    let count = r.count()?;
383    if count != expected {
384        return Err(r.malformed(at, format!("{count} records for {expected} programs")));
385    }
386    let mut out = Vec::with_capacity(count);
387    for _ in 0..count {
388        out.push(T::decode(&mut r)?);
389    }
390    r.finish()?;
391    Ok(out)
392}
393
394/// Reads and checks a module; the bytes of every section are checksummed.
395pub fn read(bytes: &[u8]) -> Result<LoadedModule, ModuleError> {
396    let module = Module::read(bytes)?;
397    let strings = module.strings()?;
398    let mut r = module.reader(Section::DIRECTORY, &strings)?;
399    let directory = Vec::<DirectoryEntry>::decode(&mut r)?;
400    r.finish()?;
401    let count = directory.len();
402
403    let options = records::<ProgramOptions>(&module, &strings, Section::OPTIONS, count)?;
404    let layouts = records::<LayoutRecord>(&module, &strings, Section::LAYOUT, count)?;
405    let bodies = lir_records(&module, &strings, count)?;
406    let sql = records::<Vec<SqlEntry>>(&module, &strings, Section::SQL, count)?;
407    let (debug, files): (Vec<Debug>, Vec<Vec<Option<SourceFile>>>) = records::<(Debug, Vec<Option<SourceFile>>)>(&module, &strings, Section::DEBUG, count)?.into_iter().unzip();
408
409    let mut r = module.reader(Section::BMS, &strings)?;
410    let mapsets = Vec::<Mapset>::decode(&mut r)?;
411    r.finish()?;
412    check_mapsets(&mapsets).map_err(|reason| bad(Section::BMS.name, reason))?;
413
414    let parts = options.into_iter().zip(layouts).zip(bodies).zip(sql).zip(debug);
415    let mut programs = Vec::with_capacity(count);
416    for ((((options, (storage, items, edits)), body), sql), debug) in parts {
417        let LirRecord {
418            id, initial, recursive, paragraphs, procedure_start, ranges, blocks, places, exprs, conds, consts, plans,
419            services, abends, symbols,
420        } = body;
421        let program = Program {
422            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places, exprs,
423            conds, consts, plans, services, sql, abends, edits, symbols, debug,
424        };
425        crate::lir::program_valid(&program).map_err(|reason| bad(Section::SQL.name, reason))?;
426        programs.push(program);
427    }
428    check_directory(&directory, &programs)?;
429    check_files(&files, &programs).map_err(|reason| bad(Section::DEBUG.name, reason))?;
430    Ok(LoadedModule { directory, programs, mapsets, files })
431}