ironwork-compile 0.9.0

ironwork for COBOL: the compiler that checks a parsed program and lays out its storage as IBM does
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! Files (lir.md ยง9.4): each file's declaration from its SELECT and FD, and each file statement as
//! `file_io` runs it, with what it finds by name on every execution resolved here.

use super::data::{Side, UNRESOLVED, Value};
use super::flow::Ctx;
use super::{Lower, LowerError, R, is_static, push, unsupported};
use crate::layout::Resolved;
use crate::printer::{self, Space};
use rt::files::Format;
use rt::lir::{
    self, Advance, FileDesc, FileOp, FileVerb, FromMove, IndexKeys, Op, Phrase, RecordDepending, RecordSpan, RelativeKey, Spacing, StartKey, StartRel, Terminator,
};
use syntax::Pos;
use syntax::ast::{Access, Advancing, Expr, FileDecl, Handlers, Linage, LinageValue, Operand, Organization, ReadStmt, Ref, RelOp, Stmt};

impl Lower<'_> {
    /// Every file's declaration, in the program's order, which file ops index.
    pub(super) fn files(&mut self) -> R<Vec<FileDesc>> {
        let program = self.program;
        let mut out = Vec::with_capacity(program.files.len());
        for (k, f) in program.files.iter().enumerate() {
            out.push(self.file_desc(k, f)?);
        }
        Ok(out)
    }

    fn file_desc(&mut self, k: usize, f: &FileDecl) -> R<FileDesc> {
        let status = match &f.status {
            None => None,
            Some(r) => match self.place(r, false) {
                Ok(place) => Some((place, self.bytes_into(place)?)),
                Err(LowerError::Unsupported(what, _)) if what == UNRESOLVED => None,
                Err(e) => return Err(e),
            },
        };
        let keys = match (f.organization, &f.record_key) {
            (Organization::Indexed, Some(prime)) => {
                let mut split = Vec::new();
                let prime = self.key_span(k, f, prime, 0, &mut split)?;
                let mut alternates = Vec::with_capacity(f.alternate_keys.len());
                for (n, (r, duplicates)) in f.alternate_keys.iter().enumerate() {
                    alternates.push((self.key_span(k, f, r, n + 1, &mut split)?, *duplicates));
                }
                Some(IndexKeys { prime, alternates, split })
            }
            (Organization::Indexed, None) => return unsupported("an indexed file without a RECORD KEY", f.pos),
            _ => None,
        };
        let depending = match &f.record_depending {
            None => None,
            Some(r) => {
                let (shortest, longest) = crate::varying_lengths(f, self.layout, k);
                Some(RecordDepending { item: self.place(r, false)?, lengths: (shortest, longest) })
            }
        };
        let relative = match &f.relative_key {
            None => None,
            Some(r) => {
                let place = self.place(r, false)?;
                let value = self.int_expr(&Expr::Operand(Operand::Ref(r.clone())), r.pos)?;
                let kind = self.kind_of(place);
                let store = self.store_plan(kind, self.place_items[place as usize])?;
                Some(RelativeKey { place, value, store, digits: kind.digits_scale().map(|(d, s)| d.saturating_sub(s)) })
            }
        };
        let counter = match self.layout.linage_counters.get(k).copied().flatten() {
            None => None,
            Some(i) => {
                let item = &self.layout.items[i];
                let r = Ref { name: item.name.clone().unwrap_or_else(|| "LINAGE-COUNTER".into()), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos: item.pos };
                let place = self.item_place(i, &r, false)?;
                if !is_static(&self.places[place as usize]) || self.places[place as usize].base != lir::Base::Program {
                    return unsupported("a LINAGE-COUNTER outside WORKING-STORAGE", f.pos);
                }
                Some((place, self.store_plan(item.kind, Some(i))?))
            }
        };
        let linage = match (&f.linage, counter) {
            (None, None) => None,
            (None, Some(_)) => return unsupported("a LINAGE-COUNTER without LINAGE", f.pos),
            (Some(Linage { lines, footing, top, bottom }), counter) => {
                let value = |lower: &mut Self, v: &Option<LinageValue>| v.as_ref().map(|v| lower.linage_value(v, f.pos)).transpose();
                Some(lir::Linage { lines: self.linage_value(lines, f.pos)?, footing: value(self, footing)?, top: value(self, top)?, bottom: value(self, bottom)?, counter })
            }
        };
        Ok(FileDesc {
            name: self.sym(&f.name),
            assign: self.sym(&f.assign),
            organization: match f.organization {
                Organization::Sequential => lir::Organization::Sequential,
                Organization::LineSequential => lir::Organization::LineSequential,
                Organization::Indexed => lir::Organization::Indexed,
                Organization::Relative => lir::Organization::Relative,
            },
            access: match f.access {
                Access::Sequential => lir::Access::Sequential,
                Access::Random => lir::Access::Random,
                Access::Dynamic => lir::Access::Dynamic,
            },
            optional: f.optional,
            format: match f.organization {
                Organization::LineSequential => Format::Text,
                _ if crate::variable_records(f, self.layout, k) => Format::Variable,
                _ => Format::Fixed,
            },
            read_lengths: crate::read_lengths(f, self.layout, k, self.c.options.vlr),
            fixed: !crate::variable_records(f, self.layout, k),
            record_min: f.record_min,
            depending,
            status,
            keys,
            relative,
            linage,
            carriage: self.c.carriage.get(k).copied().flatten().map(|c| lir::Carriage { machine: c.machine, reserved: c.reserved }),
            sort: f.sort,
            error: self.c.declaratives.files.get(k).copied().flatten().map(|s| self.span_range(s, lir::RangeKind::UseProcedure)).transpose()?,
            assign_item: match &f.assign_item {
                Some(a) => Some(lir::AssignItem { place: self.place(&a.reference, false)?, select: f.pos }),
                None => None,
            },
        })
    }

    /// A FILE STATUS that names no data item is left out of its file's declaration: the walker looks
    /// the name up only when a statement on the file sets the status, so a program none of whose
    /// statements names the file runs the same without it.
    pub(super) fn unresolved_statuses(&self) -> R<()> {
        for (k, f) in self.program.files.iter().enumerate() {
            let Some(r) = &f.status else { continue };
            if self.services.files[k].status.is_none() && self.names_file(k) {
                return unsupported("a statement on a file whose FILE STATUS names no data item", r.pos);
            }
        }
        Ok(())
    }

    /// Whether a lowered statement, SORT or MERGE, RELEASE, RETURN or report names file `k`.
    fn names_file(&self, k: usize) -> bool {
        let s = &self.services;
        let named = |f: u16| usize::from(f) == k;
        let sorted = |io: &Option<lir::SortIo>| matches!(io, Some(lir::SortIo::Files(files)) if files.iter().any(|&f| named(f)));
        s.file_ops.iter().any(|op| named(op.file))
            || s.sorts.iter().any(|plan| matches!(plan, lir::SortPlan::File(f) if named(f.sd) || sorted(&f.input) || sorted(&f.output)))
            || s.releases.iter().any(|r| r.file.is_some_and(named))
            || s.returns.iter().any(|r| r.file.is_some_and(named))
            || s.report.reports.iter().any(|r| r.file == k)
    }

    /// `assign` of a status or a record's bytes, which reach it as alphanumeric bytes.
    pub(super) fn bytes_into(&mut self, place: lir::PlaceId) -> R<lir::MovePlan> {
        self.move_plan(&Side { value: Value::Bytes, src: None, digits: 0 }, self.kind_of(place), self.place_items[place as usize])
    }

    fn linage_value(&mut self, v: &LinageValue, pos: Pos) -> R<lir::IntExpr> {
        match v {
            LinageValue::Integer(n) => n.parse().map(lir::IntExpr::Const).or_else(|_| unsupported("a LINAGE integer past 64 bits", pos)),
            LinageValue::Data(r) => self.int_expr(&Expr::Operand(Operand::Ref(r.clone())), r.pos),
        }
    }

    /// `record_span`: where a data item of file k's record area lies within the record.
    fn record_span(&mut self, k: usize, r: &Ref) -> R<RecordSpan> {
        let place = self.place(r, false)?;
        let layout = self.layout;
        let (start, size) = layout.file_areas[k];
        let q = &self.places[place as usize];
        let in_area = match q.base {
            lir::Base::Program => is_static(q) && layout.bound_areas[k].is_none(),
            // Each record bound to a file's area is bound to the same storage (machine/scope.rs).
            lir::Base::Linkage(o) => {
                let record = &layout.items[layout.linkage_roots[usize::from(o)]];
                q.moved.is_empty() && q.subscripts.is_empty() && q.odo.is_empty() && q.refmod.is_none() && !layout.is_argument(usize::from(o)) && record.file == Some(k as u16)
            }
            _ => false,
        };
        if !in_area || q.offset < start || q.offset + q.len > start + size {
            return unsupported("a file key that is not a data item of its file's record area", r.pos);
        }
        Ok(RecordSpan { offset: q.offset - start, len: q.len })
    }

    /// `key_named`: which key of indexed file k a data item names, 0 for the prime key, and its
    /// span; with `partial` (START) it may be a leading part of the key.
    /// Key number `which` of file k, named `r`: its record span, or for a split key the span of its
    /// first piece's offset and its pieces' summed length, the pieces put in `split`.
    fn key_span(&mut self, k: usize, f: &FileDecl, r: &Ref, which: usize, split: &mut Vec<(u8, Vec<RecordSpan>)>) -> R<RecordSpan> {
        let Some(items) = f.split_key(&r.name) else { return self.record_span(k, r) };
        let mut pieces = Vec::with_capacity(items.len());
        for item in items {
            pieces.push(self.record_span(k, item)?);
        }
        let joined = RecordSpan { offset: pieces[0].offset, len: pieces.iter().map(|p| p.len).sum() };
        split.push((u8::try_from(which).map_err(|_| LowerError::Unsupported("a split key past the 255th key of its file", r.pos))?, pieces));
        Ok(joined)
    }

    fn key_of_reference(&mut self, k: usize, keys: &IndexKeys, r: &Ref, partial: bool) -> R<(u8, RecordSpan)> {
        if let Some(which) = self.program.files[k].alternate_keys.iter().position(|(a, _)| a.name == r.name).map(|n| n + 1).or_else(|| self.program.files[k].record_key.as_ref().filter(|p| p.name == r.name).map(|_| 0))
            && let Some((_, pieces)) = keys.split.iter().find(|(key, _)| usize::from(*key) == which)
        {
            let span = RecordSpan { offset: pieces[0].offset, len: pieces.iter().map(|p| p.len).sum() };
            return Ok((which as u8, span));
        }
        let span = self.record_span(k, r)?;
        let fits = |key: &RecordSpan| key.offset == span.offset && (span.len == key.len || partial && span.len < key.len);
        let which = std::iter::once(&keys.prime).chain(keys.alternates.iter().map(|(s, _)| s)).position(fits);
        match which.map(u8::try_from) {
            Some(Ok(which)) => Ok((which, span)),
            _ => unsupported("a READ or START KEY that is not a key of its file", r.pos),
        }
    }

    /// The file a statement names.
    fn file_index(&self, name: &str, pos: Pos) -> R<u16> {
        match self.program.files.iter().position(|f| f.name == name).map(u16::try_from) {
            Some(Ok(k)) => Ok(k),
            _ => unsupported("a file statement naming no file", pos),
        }
    }

    /// The file a record belongs to, the record as WRITE or REWRITE writes it, and FROM's move
    /// into it as a receiving item, located first (`record_of`).
    fn record_of(&mut self, record: &Ref, from: Option<&Operand>, pos: Pos) -> R<(u16, lir::PlaceId, Option<FromMove>)> {
        let file = match self.layout.resolve(&record.name, &record.qualifiers, record.pos) {
            Ok(Resolved::Item(i)) => self.layout.items[i].file,
            _ => None,
        };
        let Some(file) = file else { return unsupported("a WRITE or REWRITE of an item that is not a file's record", pos) };
        let from = match from {
            None => None,
            Some(op) => {
                let to = self.place(record, true)?;
                let sender = self.operand(op, pos)?;
                let plan = self.move_plan(&sender.side, self.kind_of(to), self.place_items[to as usize])?;
                Some(FromMove { from: sender.operand, to, plan, check: self.move_check(sender.operand, to) })
            }
        };
        Ok((file, self.place(record, false)?, from))
    }

    /// OPEN, CLOSE, READ, WRITE, REWRITE, DELETE and START, one op per file named, each followed
    /// by its phrases.
    pub(super) fn file_statement(&mut self, s: &Stmt, pos: Pos, ctx: &Ctx) -> R<()> {
        let program = self.program;
        match s {
            Stmt::Open { files, pos: _ } => {
                for (mode, name) in files {
                    let file = self.file_index(name, pos)?;
                    self.file_op(FileOp { file, verb: FileVerb::Open(*mode), phrase: None, end_of_page: None }, [None, None, None, None], pos, ctx)?;
                }
            }
            Stmt::Close { files, pos: _ } => {
                for (name, closing) in files {
                    let file = self.file_index(name, pos)?;
                    let verb = closing.map_or(FileVerb::Close, FileVerb::CloseWith);
                    self.file_op(FileOp { file, verb, phrase: None, end_of_page: None }, [None, None, None, None], pos, ctx)?;
                }
            }
            Stmt::Read(r) => {
                let ReadStmt { file, next, previous, into, key, at_end, invalid, pos: _ } = &**r;
                let file = self.file_index(file, pos)?;
                let decl = &program.files[file as usize];
                // Only a file held in memory reads by key; any other takes AT END (`read_stream`).
                let sequential = decl.access == Access::Sequential
                    || decl.organization == Organization::Sequential
                    || decl.access == Access::Dynamic && *next
                    || decl.organization == Organization::LineSequential;
                let handlers = if sequential { at_end } else { invalid };
                let into = match into {
                    None => None,
                    Some(t) => {
                        let place = self.place(t, true)?;
                        Some((place, self.bytes_into(place)?))
                    }
                };
                let key = match (key, decl.organization) {
                    (Some(key), Organization::Indexed) if !sequential => {
                        let keys = self.index_keys(file, pos)?;
                        self.key_of_reference(file as usize, &keys, key, false)?.0
                    }
                    _ => 0,
                };
                let verb = FileVerb::Read { sequential, previous: *previous, into, key };
                self.file_op(FileOp { file, verb, phrase: phrase(handlers), end_of_page: None }, bodies(handlers, None), pos, ctx)?;
            }
            Stmt::Write { record, from, advancing, invalid, end_of_page, pos: _ } => {
                let (file, record, from) = self.record_of(record, from.as_ref(), pos)?;
                let advancing = match advancing {
                    None => None,
                    Some(a) => Some(self.advance(a, file, pos)?),
                };
                let verb = FileVerb::Write { record, from, advancing };
                let op = FileOp { file, verb, phrase: phrase(invalid), end_of_page: phrase(end_of_page) };
                self.file_op(op, bodies(invalid, Some(end_of_page)), pos, ctx)?;
            }
            Stmt::Rewrite { record, from, invalid, pos: _ } => {
                let (file, record, from) = self.record_of(record, from.as_ref(), pos)?;
                let op = FileOp { file, verb: FileVerb::Rewrite { record, from }, phrase: phrase(invalid), end_of_page: None };
                self.file_op(op, bodies(invalid, None), pos, ctx)?;
            }
            Stmt::DeleteFile { files, pos: _ } => {
                for name in files {
                    let file = self.file_index(name, pos)?;
                    self.file_op(FileOp { file, verb: FileVerb::DeleteFile, phrase: None, end_of_page: None }, [None, None, None, None], pos, ctx)?;
                }
            }
            Stmt::Delete { file, invalid, pos: _ } => {
                let file = self.file_index(file, pos)?;
                self.file_op(FileOp { file, verb: FileVerb::Delete, phrase: phrase(invalid), end_of_page: None }, bodies(invalid, None), pos, ctx)?;
            }
            Stmt::Start { file, key, invalid, pos: _ } => {
                let file = self.file_index(file, pos)?;
                let rel = match key.as_ref().map(|(op, _)| *op) {
                    None | Some(RelOp::Eq) => StartRel::Equal,
                    Some(RelOp::Gt) => StartRel::Greater,
                    Some(RelOp::Ge) => StartRel::NotLess,
                    Some(RelOp::Lt) => StartRel::Less,
                    Some(RelOp::Le) => StartRel::NotGreater,
                    Some(_) => {
                        let abend = self.ironwork("START KEY takes =, >, NOT < or >=")?;
                        return self.end(Terminator::Abend(abend), pos);
                    }
                };
                let key = match (program.files[file as usize].organization, key) {
                    (Organization::Indexed, None) => StartKey::Prime,
                    (Organization::Indexed, Some((_, r))) => {
                        let keys = self.index_keys(file, pos)?;
                        let (key, span) = self.key_of_reference(file as usize, &keys, r, true)?;
                        StartKey::Named { key, span }
                    }
                    (Organization::Relative, None) => StartKey::RelativeKey,
                    (Organization::Relative, Some((_, r))) => StartKey::Relative(self.int_expr(&Expr::Operand(Operand::Ref(r.clone())), pos)?),
                    _ => return unsupported("START on a file that is neither indexed nor relative", pos),
                };
                let op = FileOp { file, verb: FileVerb::Start { rel, key }, phrase: phrase(invalid), end_of_page: None };
                self.file_op(op, bodies(invalid, None), pos, ctx)?;
            }
            _ => return Err(super::LowerError::Invalid("a file statement that is not one".into())),
        }
        Ok(())
    }

    fn index_keys(&self, file: u16, pos: Pos) -> R<IndexKeys> {
        match self.services.files.get(file as usize).and_then(|f| f.keys.clone()) {
            Some(keys) => Ok(keys),
            None => unsupported("a key of an indexed file without a RECORD KEY", pos),
        }
    }

    /// WRITE's ADVANCING phrase, a mnemonic-name as the movement its environment-name gives.
    fn advance(&mut self, a: &Advancing, file: u16, pos: Pos) -> R<Advance> {
        Ok(match a {
            Advancing::Lines { before, count } => Advance::Lines { before: *before, count: self.int_expr(count, pos)? },
            Advancing::Page { before } => Advance::Page { before: *before },
            // The walker names the mnemonic-name only where it refuses what lowering refuses here.
            Advancing::Mnemonic { before, environment, name: _ } => {
                let space = match printer::mnemonic_space(environment) {
                    _ if self.program.files[file as usize].linage.is_some() => return unsupported("ADVANCING a mnemonic-name on a file with LINAGE", pos),
                    Some(Space::Lines(n)) => Spacing::Lines(n),
                    Some(Space::Channel(c)) => Spacing::Channel(c),
                    Some(Space::PageMode) => Spacing::PageMode,
                    None => return unsupported("ADVANCING a mnemonic-name that is not a printer channel", pos),
                };
                Advance::Mnemonic { before: *before, space }
            }
        })
    }

    /// The op, then a `Select` of its arms: none, ON, NOT ON, END-OF-PAGE and NOT END-OF-PAGE, an
    /// arm whose phrase is not written going on as none does.
    fn file_op(&mut self, op: FileOp, bodies: [Option<&[Stmt]>; 4], pos: Pos, ctx: &Ctx) -> R<()> {
        let arms = op.arms();
        let id = push(&mut self.services.file_ops, op, "file statements")?;
        self.op(Op::File(id), pos)?;
        if arms == 0 {
            return Ok(());
        }
        let join = self.new_block()?;
        let mut targets = vec![join];
        let mut runs = Vec::new();
        for body in bodies.into_iter().take(arms - 1) {
            match body {
                Some(stmts) => {
                    let b = self.new_block()?;
                    targets.push(b);
                    runs.push((b, stmts));
                }
                None => targets.push(join),
            }
        }
        self.end(Terminator::Select(targets), pos)?;
        for (b, stmts) in runs {
            self.switch(b)?;
            self.statements(stmts, ctx)?;
            self.jump(join, pos)?;
        }
        self.switch(join)
    }
}

fn phrase(h: &Handlers) -> Option<Phrase> {
    let Handlers { on, not_on } = h;
    (on.is_some() || not_on.is_some()).then_some(Phrase { on: on.is_some(), not_on: not_on.is_some() })
}

fn bodies<'s>(h: &'s Handlers, end_of_page: Option<&'s Handlers>) -> [Option<&'s [Stmt]>; 4] {
    let Handlers { on, not_on } = h;
    [on.as_deref(), not_on.as_deref(), end_of_page.and_then(|e| e.on.as_deref()), end_of_page.and_then(|e| e.not_on.as_deref())]
}