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