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