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::{
8    AbendText, Block, 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/// The programs of a module, in ordinal order, with their directory and the mapsets they use.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct LoadedModule {
58    pub directory: Vec<DirectoryEntry>,
59    pub programs: Vec<Program>,
60    pub mapsets: Vec<Mapset>,
61}
62
63/// Every field of a `Program`, listed once so a new field is a compile error here.
64struct Parts<'a> {
65    id: &'a SymId,
66    options: &'a ProgramOptions,
67    initial: &'a bool,
68    recursive: &'a bool,
69    storage: &'a Storage,
70    items: &'a Vec<Item>,
71    paragraphs: &'a Vec<Paragraph>,
72    procedure_start: &'a ParaId,
73    ranges: &'a Vec<Range>,
74    blocks: &'a Vec<Block>,
75    places: &'a Vec<Place>,
76    exprs: &'a Vec<Expr>,
77    conds: &'a Vec<Cond>,
78    consts: &'a Vec<Const>,
79    plans: &'a Plans,
80    services: &'a Services,
81    sql: &'a Vec<SqlEntry>,
82    abends: &'a Vec<AbendText>,
83    edits: &'a Vec<Edit>,
84    symbols: &'a Vec<String>,
85    debug: &'a Debug,
86}
87
88impl<'a> Parts<'a> {
89    fn of(program: &'a Program) -> Self {
90        let Program {
91            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places,
92            exprs, conds, consts, plans, services, sql, abends, edits, symbols, debug,
93        } = program;
94        Self {
95            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places,
96            exprs, conds, consts, plans, services, sql, abends, edits, symbols, debug,
97        }
98    }
99
100    fn encode_lir(&self, w: &mut Writer) {
101        self.id.encode(w);
102        self.initial.encode(w);
103        self.recursive.encode(w);
104        self.paragraphs.encode(w);
105        self.procedure_start.encode(w);
106        self.ranges.encode(w);
107        self.blocks.encode(w);
108        self.places.encode(w);
109        self.exprs.encode(w);
110        self.conds.encode(w);
111        self.consts.encode(w);
112        self.plans.encode(w);
113        self.services.encode(w);
114        self.abends.encode(w);
115        self.symbols.encode(w);
116    }
117}
118
119/// One program's record in the `LAYOUT` section.
120pub type LayoutRecord = (Storage, Vec<Item>, Vec<Edit>);
121
122/// One program's record in the `LIR` section: the fields of `Program` no other section holds.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct LirRecord {
125    pub id: SymId,
126    pub initial: bool,
127    pub recursive: bool,
128    pub paragraphs: Vec<Paragraph>,
129    pub procedure_start: ParaId,
130    pub ranges: Vec<Range>,
131    pub blocks: Vec<Block>,
132    pub places: Vec<Place>,
133    pub exprs: Vec<Expr>,
134    pub conds: Vec<Cond>,
135    pub consts: Vec<Const>,
136    pub plans: Plans,
137    pub services: Services,
138    pub abends: Vec<AbendText>,
139    pub symbols: Vec<String>,
140}
141
142impl Decode for LirRecord {
143    fn decode(r: &mut Reader<'_>) -> Result<Self, ModuleError> {
144        Ok(Self {
145            id: Decode::decode(r)?,
146            initial: Decode::decode(r)?,
147            recursive: Decode::decode(r)?,
148            paragraphs: Decode::decode(r)?,
149            procedure_start: Decode::decode(r)?,
150            ranges: Decode::decode(r)?,
151            blocks: Decode::decode(r)?,
152            places: Decode::decode(r)?,
153            exprs: Decode::decode(r)?,
154            conds: Decode::decode(r)?,
155            consts: Decode::decode(r)?,
156            plans: Decode::decode(r)?,
157            services: Decode::decode(r)?,
158            abends: Decode::decode(r)?,
159            symbols: Decode::decode(r)?,
160        })
161    }
162}
163
164/// Program count, then one record per program.
165fn per_program(w: &mut Writer, programs: &[Program], record: impl Fn(&Parts<'_>, &mut Writer)) {
166    w.count(programs.len());
167    for program in programs {
168        record(&Parts::of(program), w);
169    }
170}
171
172fn encode_module(programs: &[Program], directory: &[DirectoryEntry], mapsets: &[Mapset]) -> Vec<u8> {
173    let mut m = ModuleWriter::new();
174    m.section(Section::DIRECTORY, |w| {
175        w.count(directory.len());
176        for entry in directory {
177            entry.encode(w);
178        }
179    });
180    m.section(Section::OPTIONS, |w| per_program(w, programs, |p, w| p.options.encode(w)));
181    m.section(Section::LAYOUT, |w| {
182        per_program(w, programs, |p, w| {
183            p.storage.encode(w);
184            p.items.encode(w);
185            p.edits.encode(w);
186        });
187    });
188    m.section(Section::LIR, |w| per_program(w, programs, |p, w| p.encode_lir(w)));
189    m.section(Section::SQL, |w| per_program(w, programs, |p, w| p.sql.encode(w)));
190    m.section(Section::BMS, |w| {
191        w.count(mapsets.len());
192        for mapset in mapsets {
193            mapset.encode(w);
194        }
195    });
196    m.section(Section::DEBUG, |w| per_program(w, programs, |p, w| p.debug.encode(w)));
197    m.finish()
198}
199
200/// A module of `programs`, each a top-level program in the directory, with no mapsets. Same input,
201/// same bytes.
202pub fn write(programs: &[Program]) -> Vec<u8> {
203    let directory: Vec<_> = programs.iter().map(DirectoryEntry::top_level).collect();
204    encode_module(programs, &directory, &[])
205}
206
207/// A module with the caller's directory and mapsets, refused (as the reader would) if either, or a
208/// program, is invalid.
209pub fn write_with(programs: &[Program], directory: &[DirectoryEntry], mapsets: &[Mapset]) -> Result<Vec<u8>, ModuleError> {
210    check_directory(directory, programs)?;
211    for program in programs {
212        crate::lir::program_valid(program).map_err(|reason| bad("LIR", reason))?;
213    }
214    check_mapsets(mapsets).map_err(|reason| bad(Section::BMS.name, reason))?;
215    Ok(encode_module(programs, directory, mapsets))
216}
217
218/// Mapsets are held once each, in ascending order of name (load-module.md §5.3).
219fn check_mapsets(mapsets: &[Mapset]) -> Result<(), String> {
220    match mapsets.windows(2).find(|pair| pair[0].name >= pair[1].name) {
221        Some(pair) => Err(format!("mapset {} follows mapset {}", pair[1].name, pair[0].name)),
222        None => Ok(()),
223    }
224}
225
226fn bad(section: &'static str, reason: impl Into<String>) -> ModuleError {
227    ModuleError::Malformed { section, offset: 0, reason: reason.into() }
228}
229
230fn check_directory(directory: &[DirectoryEntry], programs: &[Program]) -> Result<(), ModuleError> {
231    let name = Section::DIRECTORY.name;
232    if directory.len() != programs.len() {
233        return Err(bad(name, format!("{} entries for {} programs", directory.len(), programs.len())));
234    }
235    for (ordinal, (entry, program)) in directory.iter().zip(programs).enumerate() {
236        let symbol = program.symbols.get(program.id as usize);
237        if symbol != Some(&entry.id) {
238            return Err(bad(name, format!("program {ordinal} is {symbol:?} in its symbols, {:?} in the directory", entry.id)));
239        }
240        if entry.parent.is_some_and(|p| p as usize >= ordinal) {
241            return Err(bad(name, format!("program {ordinal} has parent {}, which does not precede it", entry.parent.unwrap_or(0))));
242        }
243        if let Some((entry_name, _)) = entry.entries.iter().find(|(_, para)| *para as usize >= program.paragraphs.len()) {
244            return Err(bad(name, format!("program {ordinal} ENTRY {entry_name} names a paragraph it lacks")));
245        }
246    }
247    Ok(())
248}
249
250/// A section of a count and that many records, the count being the directory's.
251fn records<T: Decode>(module: &Module<'_>, strings: &StringTable, section: Section, expected: usize) -> Result<Vec<T>, ModuleError> {
252    let mut r = module.reader(section, strings)?;
253    let at = r.position();
254    let count = r.count()?;
255    if count != expected {
256        return Err(r.malformed(at, format!("{count} records for {expected} programs")));
257    }
258    let mut out = Vec::with_capacity(count);
259    for _ in 0..count {
260        out.push(T::decode(&mut r)?);
261    }
262    r.finish()?;
263    Ok(out)
264}
265
266/// Reads and checks a module; the bytes of every section are checksummed.
267pub fn read(bytes: &[u8]) -> Result<LoadedModule, ModuleError> {
268    let module = Module::read(bytes)?;
269    let strings = module.strings()?;
270    let mut r = module.reader(Section::DIRECTORY, &strings)?;
271    let directory = Vec::<DirectoryEntry>::decode(&mut r)?;
272    r.finish()?;
273    let count = directory.len();
274
275    let options = records::<ProgramOptions>(&module, &strings, Section::OPTIONS, count)?;
276    let layouts = records::<LayoutRecord>(&module, &strings, Section::LAYOUT, count)?;
277    let bodies = records::<LirRecord>(&module, &strings, Section::LIR, count)?;
278    let sql = records::<Vec<SqlEntry>>(&module, &strings, Section::SQL, count)?;
279    let debug = records::<Debug>(&module, &strings, Section::DEBUG, count)?;
280
281    let mut r = module.reader(Section::BMS, &strings)?;
282    let mapsets = Vec::<Mapset>::decode(&mut r)?;
283    r.finish()?;
284    check_mapsets(&mapsets).map_err(|reason| bad(Section::BMS.name, reason))?;
285
286    let parts = options.into_iter().zip(layouts).zip(bodies).zip(sql).zip(debug);
287    let mut programs = Vec::with_capacity(count);
288    for ((((options, (storage, items, edits)), body), sql), debug) in parts {
289        let LirRecord {
290            id, initial, recursive, paragraphs, procedure_start, ranges, blocks, places, exprs, conds, consts, plans,
291            services, abends, symbols,
292        } = body;
293        let program = Program {
294            id, options, initial, recursive, storage, items, paragraphs, procedure_start, ranges, blocks, places, exprs,
295            conds, consts, plans, services, sql, abends, edits, symbols, debug,
296        };
297        crate::lir::program_valid(&program).map_err(|reason| bad(Section::SQL.name, reason))?;
298        programs.push(program);
299    }
300    check_directory(&directory, &programs)?;
301    Ok(LoadedModule { directory, programs, mapsets })
302}