Skip to main content

ironwork_exec/
layout.rs

1//! WORKING-STORAGE as IBM lays it out: sizes by USAGE and PICTURE, REDEFINES sharing storage,
2//! OCCURS repeating it. See [`numeric::assumptions::WORKING_STORAGE_LAYOUT`] for where each 01
3//! level starts.
4
5use crate::picture::{self, Category, Sym};
6use syntax::ast::{DataEntry, Literal, SignClause, Usage};
7use syntax::{Error, Pos};
8use zarch::hfp::Precision;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Kind {
12    Group,
13    Alnum { justified: bool },
14    National,
15    Zoned { digits: u32, scale: u32, signed: bool, sign: Option<SignClause> },
16    Packed { digits: u32, scale: u32, signed: bool },
17    Binary { digits: u32, scale: u32, signed: bool, native: bool },
18    Float(Precision),
19    /// `edit` indexes [`Layout::edits`].
20    NumericEdited { edit: u32, digits: u32, scale: u32, blank_when_zero: bool },
21    AlnumEdited { edit: u32 },
22    /// USAGE POINTER: an address, four bytes.
23    Pointer,
24    /// An index name or USAGE INDEX item, holding an occurrence number in four bytes.
25    Index,
26}
27
28impl Kind {
29    pub fn is_numeric(self) -> bool {
30        matches!(self, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_) | Kind::Index)
31    }
32
33    /// Digits and decimal places of a fixed-point numeric item.
34    pub fn digits_scale(self) -> Option<(u32, u32)> {
35        match self {
36            Kind::Zoned { digits, scale, .. }
37            | Kind::Packed { digits, scale, .. }
38            | Kind::Binary { digits, scale, .. }
39            | Kind::NumericEdited { digits, scale, .. } => Some((digits, scale)),
40            Kind::Index => Some((9, 0)),
41            _ => None,
42        }
43    }
44}
45
46#[derive(Clone, Debug)]
47pub struct Item {
48    pub name: Option<String>,
49    pub level: u8,
50    pub parent: Option<usize>,
51    pub children: Vec<usize>,
52    pub offset: u32,
53    /// One occurrence.
54    pub size: u32,
55    pub occurs: u32,
56    /// Declared with OCCURS, so references to it take a subscript.
57    pub table: bool,
58    /// OCCURS ... DEPENDING ON: the item holding the current number of occurrences.
59    pub depending_on: Option<syntax::ast::Ref>,
60    /// The OCCURS DEPENDING ON table this group ends with, whose current count sets its length.
61    pub odo: Option<usize>,
62    /// A table's INDEXED BY names, and its ASCENDING/DESCENDING keys.
63    pub index_names: Vec<String>,
64    pub keys: Vec<(bool, syntax::ast::Ref)>,
65    /// A LOCAL-STORAGE item: its offset is from the activation's local storage.
66    pub local: bool,
67    pub kind: Kind,
68    pub value: Option<Literal>,
69    /// Stride and count of each OCCURS on the item and its ancestors, outermost first.
70    pub dims: Vec<(u32, u32)>,
71    pub redefines: Option<String>,
72    /// The file whose record area holds the item, for a FILE SECTION record.
73    pub file: Option<u16>,
74    /// The LINKAGE record the item belongs to, by its position among LINKAGE 01 and 77 items; its
75    /// offset is from that record's start, wherever the caller's argument puts it.
76    pub linkage: Option<u16>,
77    pub pos: Pos,
78}
79
80#[derive(Clone, Debug)]
81pub struct Condition {
82    pub name: String,
83    pub item: usize,
84    pub values: Vec<(Literal, Option<Literal>)>,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Resolved {
89    Item(usize),
90    Condition(usize),
91}
92
93pub struct Layout {
94    pub items: Vec<Item>,
95    pub conditions: Vec<Condition>,
96    /// The positions of each edited PICTURE.
97    pub edits: Vec<Vec<Sym>>,
98    /// Offset and size of each file's record area, in declaration order.
99    pub file_areas: Vec<(u32, u32)>,
100    /// The item of each LINKAGE record, in order.
101    pub linkage_roots: Vec<usize>,
102    /// Bytes of LOCAL-STORAGE each activation gets.
103    pub local_size: u32,
104    pub size: u32,
105}
106
107const LEVEL_ALIGNMENT: u32 = 8;
108/// The interpreter's ceiling on WORKING-STORAGE, well under Enterprise COBOL's own.
109pub const MAX_STORAGE: u32 = 128 << 20;
110
111/// Lays out WORKING-STORAGE, then each file's record area, which all its 01 records share and
112/// which is at least `record_max` bytes.
113pub fn build(entries: &[DataEntry], files: &[(&[DataEntry], Option<u32>)], linkage: &[DataEntry], local: &[DataEntry]) -> Result<Layout, Error> {
114    let mut items: Vec<Item> = Vec::new();
115    let mut usages: Vec<Option<Usage>> = Vec::new();
116    let mut conditions = Vec::new();
117    let mut open: Vec<usize> = Vec::new();
118    const LINKAGE: u16 = u16::MAX;
119    const LOCAL: u16 = u16::MAX - 1;
120    let tagged: Vec<(Option<u16>, &DataEntry)> = entries
121        .iter()
122        .map(|e| (None, e))
123        .chain(files.iter().enumerate().flat_map(|(k, (records, _))| records.iter().map(move |e| (Some(k as u16), e))))
124        .chain(linkage.iter().map(|e| (Some(LINKAGE), e)))
125        .chain(local.iter().map(|e| (Some(LOCAL), e)))
126        .collect();
127    let mut group = None;
128    let mut linkage_roots = Vec::new();
129    for &(region, e) in &tagged {
130        if region != group {
131            open.clear();
132            group = region;
133        }
134        let file = region.filter(|&r| r != LINKAGE && r != LOCAL);
135        let in_linkage = region == Some(LINKAGE);
136
137
138        if e.level == 88 {
139            let item = *open.last().ok_or_else(|| Error::at(e.pos, "a level-88 entry with no item before it"))?;
140            let name = e.name.clone().ok_or_else(|| Error::at(e.pos, "a level-88 entry needs a name"))?;
141            conditions.push(Condition { name, item, values: e.condition_values.clone() });
142            continue;
143        }
144        if e.level == 66 {
145            return Err(Error::at(e.pos, "RENAMES (level 66) is not supported yet"));
146        }
147        if !(e.level == 1 || e.level == 77 || (2..=49).contains(&e.level)) {
148            return Err(Error::at(e.pos, format!("level {} is not a data level", e.level)));
149        }
150        if e.sync {
151            return Err(Error::at(e.pos, "SYNCHRONIZED is not supported yet"));
152        }
153        while open.last().is_some_and(|&i| items[i].level >= e.level || items[i].level == 77) {
154            open.pop();
155        }
156        let parent = if e.level == 1 || e.level == 77 { None } else { open.last().copied() };
157        if e.level != 1 && e.level != 77 && parent.is_none() {
158            return Err(Error::at(e.pos, format!("level {} with no group to belong to", e.level)));
159        }
160        let index = items.len();
161        items.push(Item {
162            name: e.name.clone(),
163            level: e.level,
164            parent,
165            children: Vec::new(),
166            offset: 0,
167            size: 0,
168            occurs: e.occurs.unwrap_or(1),
169            table: e.occurs.is_some(),
170            depending_on: e.depending_on.clone(),
171            odo: None,
172            index_names: e.indexed_by.clone(),
173            keys: e.keys.clone(),
174            local: region == Some(LOCAL),
175            kind: Kind::Group,
176            value: e.value.clone().filter(|_| file.is_none() && !in_linkage),
177            dims: Vec::new(),
178            redefines: e.redefines.clone(),
179            file,
180            linkage: if in_linkage {
181                Some(match parent {
182                    None => {
183                        linkage_roots.push(index);
184                        linkage_roots.len() as u16 - 1
185                    }
186                    Some(p) => items[p].linkage.unwrap_or_default(),
187                })
188            } else {
189                None
190            },
191            pos: e.pos,
192        });
193        if (e.level == 1 || e.level == 77) && e.occurs.is_some() {
194            return Err(Error::at(e.pos, "OCCURS is not allowed at level 01 or 77"));
195        }
196        if e.occurs == Some(0) {
197            return Err(Error::at(e.pos, "OCCURS 0 is not a table"));
198        }
199        let inherited = parent.and_then(|p| usages[p]);
200        usages.push(e.usage.or(inherited));
201        if let Some(p) = parent {
202            items[p].children.push(index);
203        }
204        open.push(index);
205    }
206    let mut edits = Vec::new();
207    for (index, (_, e)) in tagged.iter().filter(|(_, e)| e.level != 88).enumerate() {
208        items[index].kind = kind(e, &items[index], usages[index], &mut edits)?;
209        items[index].size = elementary_size(&items[index], declared_size(e));
210    }
211    for (_, e) in &tagged {
212        for name in &e.indexed_by {
213            items.push(Item {
214                name: Some(name.clone()),
215                level: 77,
216                parent: None,
217                children: Vec::new(),
218                offset: 0,
219                size: 4,
220                occurs: 1,
221                table: false,
222                depending_on: None,
223                odo: None,
224                index_names: Vec::new(),
225                keys: Vec::new(),
226                local: false,
227                kind: Kind::Index,
228                value: None,
229                dims: Vec::new(),
230                redefines: None,
231                file: None,
232                linkage: None,
233                pos: e.pos,
234            });
235        }
236    }
237    let roots: Vec<usize> = (0..items.len()).filter(|&i| items[i].parent.is_none()).collect();
238    for &r in &roots {
239        measure(&mut items, r)?;
240    }
241    let mut local_cursor = 0u32;
242    let local_roots: Vec<usize> = roots.iter().copied().filter(|&r| items[r].local).collect();
243    for r in local_roots {
244        let start = local_cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT;
245        local_cursor = start + items[r].size;
246        if local_cursor > MAX_STORAGE {
247            return Err(Error::at(items[r].pos, format!("LOCAL-STORAGE exceeds the interpreter's {MAX_STORAGE} bytes")));
248        }
249        place(&mut items, r, start, Vec::new());
250    }
251    let mut cursor = 0u32;
252    let mut root_offsets: Vec<(String, u32)> = Vec::new();
253    let mut file_areas: Vec<Option<(u32, u32)>> = vec![None; files.len()];
254    for &r in &roots {
255        if items[r].local {
256            continue;
257        }
258        if items[r].linkage.is_some() {
259            place(&mut items, r, 0, Vec::new());
260            continue;
261        }
262        if let Some(k) = items[r].file {
263            let k = k as usize;
264            let (start, size) = *file_areas[k].get_or_insert((cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT, files[k].1.unwrap_or(0)));
265            let size = size.max(items[r].size);
266            file_areas[k] = Some((start, size));
267            cursor = cursor.max(start + size);
268            if cursor > MAX_STORAGE {
269                return Err(Error::at(items[r].pos, format!("storage exceeds the interpreter's {MAX_STORAGE} bytes")));
270            }
271            place(&mut items, r, start, Vec::new());
272            continue;
273        }
274        let offset = match &items[r].redefines {
275            Some(target) => root_offsets.iter().find(|(n, _)| n == target).map(|&(_, o)| o).ok_or_else(|| {
276                Error::at(items[r].pos, format!("REDEFINES {target}: no earlier 01-level item of that name"))
277            })?,
278            None => cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT,
279        };
280        cursor = cursor.max(offset + items[r].size);
281        if cursor > MAX_STORAGE {
282            return Err(Error::at(items[r].pos, format!("WORKING-STORAGE exceeds the interpreter's {MAX_STORAGE} bytes")));
283        }
284        if let Some(name) = &items[r].name {
285            root_offsets.push((name.clone(), offset));
286        }
287        place(&mut items, r, offset, Vec::new());
288    }
289    let tables: Vec<usize> = (0..items.len()).filter(|&i| items[i].depending_on.is_some()).collect();
290    for t in tables {
291        let (mut child, mut at) = (t, items[t].parent);
292        while let Some(a) = at {
293            let last = items[a].children.iter().rev().find(|&&c| items[c].redefines.is_none()).copied();
294            if last != Some(child) {
295                return Err(Error::at(items[t].pos, "items after an OCCURS DEPENDING ON table in the same record are not supported yet"));
296            }
297            items[a].odo.get_or_insert(t);
298            (child, at) = (a, items[a].parent);
299        }
300    }
301    let mut areas = Vec::new();
302    for (k, area) in file_areas.into_iter().enumerate() {
303        let area = area.unwrap_or_else(|| {
304            let start = cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT;
305            (start, files[k].1.unwrap_or(0))
306        });
307        cursor = cursor.max(area.0 + area.1);
308        areas.push(area);
309    }
310    Ok(Layout { items, conditions, edits, file_areas: areas, linkage_roots, local_size: local_cursor, size: cursor })
311}
312
313fn kind(e: &DataEntry, item: &Item, usage: Option<Usage>, edits: &mut Vec<Vec<Sym>>) -> Result<Kind, Error> {
314    let err = |m: String| Error::at(e.pos, m);
315    let usage = usage.unwrap_or_default();
316    let elementary = e.picture.is_some() || matches!(usage, Usage::Float1 | Usage::Float2 | Usage::Pointer | Usage::Index) && item.children.is_empty();
317    if !elementary {
318        return if item.children.is_empty() { Err(err("an elementary item needs a PICTURE".into())) } else { Ok(Kind::Group) };
319    }
320    if !item.children.is_empty() {
321        return Err(err("a group item cannot have a PICTURE".into()));
322    }
323    if let Usage::Pointer | Usage::Index = usage {
324        if e.picture.is_some() {
325            return Err(err("POINTER and INDEX items take no PICTURE".into()));
326        }
327        return Ok(if usage == Usage::Pointer { Kind::Pointer } else { Kind::Index });
328    }
329    if let Usage::Float1 | Usage::Float2 = usage {
330        if e.picture.is_some() {
331            return Err(err("COMP-1 and COMP-2 items take no PICTURE".into()));
332        }
333        return Ok(Kind::Float(if usage == Usage::Float1 { Precision::Short } else { Precision::Long }));
334    }
335    let pic = picture::analyse(e.picture.as_deref().unwrap()).map_err(err)?;
336    if e.blank_when_zero && pic.category != Category::NumericEdited {
337        return Err(err("BLANK WHEN ZERO is supported on numeric-edited items only, so far".into()));
338    }
339    let k = match (pic.category, usage) {
340        (Category::NumericEdited, Usage::Display) => {
341            edits.push(pic.edit.clone().unwrap_or_default());
342            Kind::NumericEdited { edit: edits.len() as u32 - 1, digits: pic.digits, scale: pic.scale, blank_when_zero: e.blank_when_zero }
343        }
344        (Category::AlphanumericEdited, Usage::Display) => {
345            edits.push(pic.edit.clone().unwrap_or_default());
346            Kind::AlnumEdited { edit: edits.len() as u32 - 1 }
347        }
348        (Category::Numeric, Usage::Display) => Kind::Zoned { digits: pic.digits, scale: pic.scale, signed: pic.signed, sign: e.sign },
349        (Category::Numeric, Usage::Packed) => Kind::Packed { digits: pic.digits, scale: pic.scale, signed: pic.signed },
350        (Category::Numeric, Usage::Binary | Usage::NativeBinary) if pic.digits <= 18 => {
351            Kind::Binary { digits: pic.digits, scale: pic.scale, signed: pic.signed, native: usage == Usage::NativeBinary }
352        }
353        (Category::Numeric, Usage::Binary | Usage::NativeBinary) => return Err(err("a binary item holds at most 18 digits".into())),
354        (Category::Alphanumeric, Usage::Display) => Kind::Alnum { justified: e.justified },
355        (Category::National, Usage::Display | Usage::National) => Kind::National,
356        (category, usage) => return Err(err(format!("a {category:?} PICTURE with USAGE {usage:?} is not supported yet"))),
357    };
358    if let Kind::Zoned { sign: Some(_), signed: false, .. } = k {
359        return Err(err("a SIGN clause needs an S in the PICTURE".into()));
360    }
361    Ok(k)
362}
363
364fn elementary_size(item: &Item, e_size: Option<u32>) -> u32 {
365    match item.kind {
366        Kind::Group => 0,
367        Kind::Alnum { .. } | Kind::NumericEdited { .. } | Kind::AlnumEdited { .. } => e_size.unwrap_or(0),
368        Kind::National => 2 * e_size.unwrap_or(0),
369        Kind::Zoned { digits, sign, .. } => digits + sign.is_some_and(|s| s.separate) as u32,
370        Kind::Packed { digits, .. } => digits / 2 + 1,
371        Kind::Binary { digits, .. } => match digits {
372            0..=4 => 2,
373            5..=9 => 4,
374            _ => 8,
375        },
376        Kind::Float(p) => p.bytes() as u32,
377        Kind::Pointer | Kind::Index => 4,
378    }
379}
380
381/// Sizes the item and its descendants; children's offsets are relative to their parent here.
382fn measure(items: &mut [Item], index: usize) -> Result<(), Error> {
383    if items[index].kind != Kind::Group {
384        return Ok(());
385    }
386    let children = items[index].children.clone();
387    let (mut cursor, mut extent) = (0u32, 0u32);
388    let mut placed: Vec<(Option<String>, u32)> = Vec::new();
389    for c in children {
390        measure(items, c)?;
391        let offset = match items[c].redefines.clone() {
392            Some(target) => placed.iter().rev().find(|(n, _)| n.as_deref() == Some(target.as_str())).map(|&(_, o)| o).ok_or_else(|| {
393                Error::at(items[c].pos, format!("REDEFINES {target}: no earlier item of that name at this level"))
394            })?,
395            None => cursor,
396        };
397        let too_large = || Error::at(items[c].pos, format!("an item larger than the interpreter's {MAX_STORAGE} bytes"));
398        let span = items[c].size.checked_mul(items[c].occurs).filter(|&s| s <= MAX_STORAGE).ok_or_else(too_large)?;
399        if items[c].redefines.is_none() {
400            cursor = cursor.checked_add(span).filter(|&s| s <= MAX_STORAGE).ok_or_else(too_large)?;
401        }
402        extent = extent.max(offset + span);
403        items[c].offset = offset;
404        placed.push((items[c].name.clone(), offset));
405    }
406    items[index].size = cursor.max(extent);
407    Ok(())
408}
409
410fn place(items: &mut [Item], index: usize, offset: u32, mut dims: Vec<(u32, u32)>) {
411    items[index].offset = offset;
412    if items[index].table {
413        dims.push((items[index].size, items[index].occurs));
414    }
415    items[index].dims = dims.clone();
416    for c in items[index].children.clone() {
417        let relative = items[c].offset;
418        place(items, c, offset + relative, dims.clone());
419    }
420}
421
422impl Layout {
423    pub fn resolve(&self, name: &str, qualifiers: &[String], pos: Pos) -> Result<Resolved, Error> {
424        let within = |mut at: Option<usize>| {
425            let mut wanted = qualifiers.iter();
426            let mut next = wanted.next();
427            while let (Some(q), Some(i)) = (next, at) {
428                if self.items[i].name.as_deref() == Some(q.as_str()) {
429                    next = wanted.next();
430                }
431                at = self.items[i].parent;
432            }
433            next.is_none()
434        };
435        let mut found: Vec<Resolved> = self
436            .items
437            .iter()
438            .enumerate()
439            .filter(|(_, it)| it.name.as_deref() == Some(name) && within(it.parent))
440            .map(|(i, _)| Resolved::Item(i))
441            .collect();
442        found.extend(
443            self.conditions.iter().enumerate().filter(|(_, c)| c.name == name && within(Some(c.item))).map(|(i, _)| Resolved::Condition(i)),
444        );
445        match found.as_slice() {
446            [one] => Ok(*one),
447            [] => Err(Error::at(pos, format!("{name} is not defined"))),
448            _ => Err(Error::at(pos, format!("{name} is ambiguous; qualify it with OF or IN"))),
449        }
450    }
451}
452
453fn declared_size(e: &DataEntry) -> Option<u32> {
454    e.picture.as_deref().and_then(|p| picture::analyse(p).ok()).map(|p| p.size)
455}