Skip to main content

ironwork_compile/
layout.rs

1//! WORKING-STORAGE as IBM lays it out: sizes by USAGE and PICTURE, REDEFINES sharing storage,
2//! OCCURS repeating it, SYNCHRONIZED slack bytes before an item and after each occurrence, and
3//! level-66 RENAMES over what is laid out. See [`numeric::assumptions::WORKING_STORAGE_LAYOUT`]
4//! for where each 01 level starts.
5
6use crate::picture::{self, Category, Sym};
7use numeric::Qualify;
8use syntax::ast::{DataEntry, Environment, FileDecl, Literal, Organization, Ref, Usage};
9use syntax::{Error, Pos};
10use zarch::hfp::Precision;
11
12pub use rt::storage::Kind;
13
14#[derive(Clone, Debug)]
15pub struct Item {
16    pub name: Option<String>,
17    pub level: u8,
18    pub parent: Option<usize>,
19    pub children: Vec<usize>,
20    pub offset: u32,
21    /// One occurrence.
22    pub size: u32,
23    pub occurs: u32,
24    /// The fewest occurrences: OCCURS DEPENDING ON's minimum, else `occurs`.
25    pub occurs_min: u32,
26    /// Declared with OCCURS, so references to it take a subscript.
27    pub table: bool,
28    /// OCCURS ... DEPENDING ON: the item holding the current number of occurrences.
29    pub depending_on: Option<syntax::ast::Ref>,
30    /// The OCCURS DEPENDING ON table this group ends with, whose current count sets its length.
31    pub odo: Option<usize>,
32    /// A table's INDEXED BY names, and its ASCENDING/DESCENDING keys.
33    pub index_names: Vec<String>,
34    pub keys: Vec<(bool, syntax::ast::Ref)>,
35    /// A LOCAL-STORAGE item: its offset is from the activation's local storage.
36    pub local: bool,
37    pub kind: Kind,
38    pub value: Option<Literal>,
39    /// Stride and count of each OCCURS on the item and its ancestors, outermost first.
40    pub dims: Vec<(u32, u32)>,
41    pub redefines: Option<String>,
42    /// The file whose record area holds the item, for a FILE SECTION record.
43    pub file: Option<u16>,
44    /// The LINKAGE record the item belongs to, by its position among LINKAGE 01 and 77 items; its
45    /// offset is from that record's start, wherever the caller's argument puts it.
46    pub linkage: Option<u16>,
47    /// The class-name of a typed object reference.
48    pub object_class: Option<String>,
49    /// PICTURE scaling positions P right of the digits: the item's value is its digits times ten
50    /// to this power.
51    pub scaling: u32,
52    pub pos: Pos,
53}
54
55#[derive(Clone, Debug)]
56pub struct Condition {
57    pub name: String,
58    pub item: usize,
59    pub values: Vec<(Literal, Option<Literal>)>,
60    /// The WHEN SET TO FALSE value.
61    pub false_value: Option<Literal>,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Resolved {
66    Item(usize),
67    Condition(usize),
68}
69
70pub struct Layout {
71    pub items: Vec<Item>,
72    pub conditions: Vec<Condition>,
73    /// The positions of each edited PICTURE.
74    pub edits: Vec<Vec<Sym>>,
75    /// The currency sign value each edited PICTURE's currency symbol stands for, empty without one.
76    pub currencies: Vec<String>,
77    /// Offset and size of each file's record area, in declaration order.
78    pub file_areas: Vec<(u32, u32)>,
79    /// The least and greatest length of each file's level-01 records, an OCCURS DEPENDING ON table
80    /// counted at its fewest and at its most occurrences (Language Reference SC27-8713-03, p. 188);
81    /// None for a file with none.
82    pub record_lengths: Vec<Option<(u32, u32)>>,
83    /// The item of each LINKAGE record, in order.
84    pub linkage_roots: Vec<usize>,
85    /// Bytes of LOCAL-STORAGE each activation gets.
86    pub local_size: u32,
87    pub size: u32,
88    /// Each FD and SD name, in declaration order: the highest qualifier of its records.
89    pub file_names: Vec<String>,
90    /// The LINAGE-COUNTER item of each file whose FD has LINAGE, which its file-name qualifies.
91    pub linage_counters: Vec<Option<usize>>,
92    /// The QUALIFY option [`Layout::resolve`] follows.
93    pub qualify: Qualify,
94}
95
96const LEVEL_ALIGNMENT: u32 = 8;
97/// The interpreter's ceiling on WORKING-STORAGE, well under Enterprise COBOL's own.
98pub const MAX_STORAGE: u32 = 128 << 20;
99
100/// Lays out WORKING-STORAGE, then each file's record area, which all its 01 records share and
101/// which is at least `record_max` bytes. Files whose `shared` entry names the same file share one
102/// area, as large as the largest of them (see [`record_area_owners`]). `notation` is what
103/// SPECIAL-NAMES changes in the PICTUREs; `qualify` how RENAMES and later references resolve.
104pub fn build(
105    entries: &[DataEntry],
106    files: &[(&[DataEntry], Option<u32>)],
107    shared: &[usize],
108    linkage: &[DataEntry],
109    local: &[DataEntry],
110    notation: picture::Notation,
111    qualify: Qualify,
112) -> Result<Layout, Error> {
113    let mut items: Vec<Item> = Vec::new();
114    let mut usages: Vec<Option<Usage>> = Vec::new();
115    let mut synchronized: Vec<bool> = Vec::new();
116    let mut conditions = Vec::new();
117    let mut open: Vec<usize> = Vec::new();
118    let mut renames: Vec<(usize, &DataEntry)> = Vec::new();
119    const LINKAGE: u16 = u16::MAX;
120    const LOCAL: u16 = u16::MAX - 1;
121    let tagged: Vec<(Option<u16>, &DataEntry)> = entries
122        .iter()
123        .map(|e| (None, e))
124        .chain(files.iter().enumerate().flat_map(|(k, (records, _))| records.iter().map(move |e| (Some(k as u16), e))))
125        .chain(linkage.iter().map(|e| (Some(LINKAGE), e)))
126        .chain(local.iter().map(|e| (Some(LOCAL), e)))
127        .collect();
128    let mut group = None;
129    let mut linkage_roots = Vec::new();
130    let mut after_renames = false;
131    for &(region, e) in &tagged {
132        if region != group {
133            open.clear();
134            group = region;
135            after_renames = false;
136        }
137        let file = region.filter(|&r| r != LINKAGE && r != LOCAL);
138        let in_linkage = region == Some(LINKAGE);
139        if e.occurs.is_some() && matches!(e.level, 1 | 66 | 77 | 88) {
140            return Err(Error::at(e.pos, format!("OCCURS at level {:02}: Enterprise COBOL takes OCCURS only at levels 02 to 49", e.level)));
141        }
142        if e.level == 88 {
143            if after_renames {
144                return Err(Error::at(e.pos, "a level-88 entry after a level-66 entry: a RENAMES item cannot be a conditional variable"));
145            }
146            let item = *open.last().ok_or_else(|| Error::at(e.pos, "a level-88 entry with no item before it"))?;
147            let name = e.name.clone().ok_or_else(|| Error::at(e.pos, "a level-88 entry needs a name"))?;
148            conditions.push(Condition { name, item, values: e.condition_values.clone(), false_value: e.false_value.clone() });
149            continue;
150        }
151        if e.renames.is_some() != (e.level == 66) {
152            return Err(Error::at(e.pos, "RENAMES goes with level 66, and level 66 with RENAMES"));
153        }
154        if e.level == 66 {
155            let record = open.first().copied().filter(|&r| items[r].level == 1).ok_or_else(|| Error::at(e.pos, "a level-66 entry must follow the entries of a level-01 record"))?;
156            if e.name.is_none() || e.picture.is_some() || e.usage.is_some() || e.value.is_some() || e.redefines.is_some() || e.sync || e.sign.is_some() {
157                return Err(Error::at(e.pos, "a level-66 entry has a name and a RENAMES clause, and nothing else"));
158            }
159            renames.push((items.len(), e));
160            items.push(Item {
161                name: e.name.clone(),
162                level: 66,
163                parent: Some(record),
164                children: Vec::new(),
165                offset: 0,
166                size: 0,
167                occurs: 1,
168                occurs_min: 1,
169                table: false,
170                depending_on: None,
171                odo: None,
172                index_names: Vec::new(),
173                keys: Vec::new(),
174                local: items[record].local,
175                kind: Kind::Group,
176                value: None,
177                dims: Vec::new(),
178                redefines: None,
179                file,
180                linkage: items[record].linkage,
181                object_class: None,
182                scaling: 0,
183                pos: e.pos,
184            });
185            usages.push(None);
186            synchronized.push(false);
187            after_renames = true;
188            continue;
189        }
190        if !(e.level == 1 || e.level == 77 || (2..=49).contains(&e.level)) {
191            return Err(Error::at(e.pos, format!("level {} is not a data level", e.level)));
192        }
193        if after_renames && e.level != 1 && e.level != 77 {
194            return Err(Error::at(e.pos, format!("level {:02} after a level-66 entry: a record's RENAMES entries follow its last entry", e.level)));
195        }
196        after_renames = false;
197        while open.last().is_some_and(|&i| items[i].level >= e.level || items[i].level == 77) {
198            open.pop();
199        }
200        let parent = if e.level == 1 || e.level == 77 { None } else { open.last().copied() };
201        if e.level != 1 && e.level != 77 && parent.is_none() {
202            return Err(Error::at(e.pos, format!("level {} with no group to belong to", e.level)));
203        }
204        let index = items.len();
205        items.push(Item {
206            name: e.name.clone(),
207            level: e.level,
208            parent,
209            children: Vec::new(),
210            offset: 0,
211            size: 0,
212            occurs: e.occurs.unwrap_or(1),
213            occurs_min: e.occurs_min.or(e.occurs).unwrap_or(1),
214            table: e.occurs.is_some(),
215            depending_on: e.depending_on.clone(),
216            odo: None,
217            index_names: e.indexed_by.clone(),
218            keys: e.keys.clone(),
219            local: region == Some(LOCAL),
220            kind: Kind::Group,
221            value: e.value.clone().filter(|_| file.is_none() && !in_linkage),
222            dims: Vec::new(),
223            redefines: e.redefines.clone(),
224            file,
225            linkage: if in_linkage {
226                Some(match parent {
227                    None => {
228                        linkage_roots.push(index);
229                        linkage_roots.len() as u16 - 1
230                    }
231                    Some(p) => items[p].linkage.unwrap_or_default(),
232                })
233            } else {
234                None
235            },
236            object_class: e.object_class.clone(),
237            scaling: 0,
238            pos: e.pos,
239        });
240        if e.occurs == Some(0) {
241            return Err(Error::at(e.pos, "OCCURS 0 is not a table"));
242        }
243        let inherited = parent.and_then(|p| usages[p]);
244        usages.push(e.usage.or(inherited));
245        synchronized.push(e.sync || parent.is_some_and(|p| synchronized[p]));
246        if let Some(p) = parent {
247            items[p].children.push(index);
248        }
249        open.push(index);
250    }
251    let (mut edits, mut currencies) = (Vec::new(), Vec::new());
252    let mut aligns = vec![1u32; items.len()];
253    for (index, (_, e)) in tagged.iter().filter(|(_, e)| e.level != 88).enumerate() {
254        if e.level == 66 {
255            continue;
256        }
257        let pic = e.picture.as_deref().map(|p| picture::analyse_with(p, notation).map_err(|m| Error::at(e.pos, m))).transpose()?;
258        items[index].kind = kind(e, &items[index], usages[index], pic.as_ref(), &mut edits)?;
259        if currencies.len() < edits.len() {
260            currencies.push(pic.as_ref().and_then(|p| p.currency.clone()).unwrap_or_default());
261        }
262        items[index].scaling = pic.as_ref().map_or(0, |p| p.scaling);
263        items[index].size = elementary_size(&items[index], pic.as_ref().map(|p| p.size));
264        if synchronized[index] && items[index].kind != Kind::Group {
265            aligns[index] = alignment(items[index].kind);
266        }
267    }
268    for (_, e) in &tagged {
269        for name in &e.indexed_by {
270            items.push(Item {
271                name: Some(name.clone()),
272                level: 77,
273                parent: None,
274                children: Vec::new(),
275                offset: 0,
276                size: 4,
277                occurs: 1,
278                occurs_min: 1,
279                table: false,
280                depending_on: None,
281                odo: None,
282                index_names: Vec::new(),
283                keys: Vec::new(),
284                local: false,
285                kind: Kind::Index,
286                value: None,
287                dims: Vec::new(),
288                redefines: None,
289                file: None,
290                linkage: None,
291                object_class: None,
292                scaling: 0,
293                pos: e.pos,
294            });
295        }
296    }
297    aligns.resize(items.len(), 1);
298    let roots: Vec<usize> = (0..items.len()).filter(|&i| items[i].parent.is_none()).collect();
299    for &r in &roots {
300        measure(&mut items, &aligns, r, 0)?;
301    }
302    let mut local_cursor = 0u32;
303    let local_roots: Vec<usize> = roots.iter().copied().filter(|&r| items[r].local).collect();
304    for r in local_roots {
305        let start = local_cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT;
306        local_cursor = start + items[r].size;
307        if local_cursor > MAX_STORAGE {
308            return Err(Error::at(items[r].pos, format!("LOCAL-STORAGE exceeds the interpreter's {MAX_STORAGE} bytes")));
309        }
310        place(&mut items, r, start, Vec::new());
311    }
312    let owner = |k: usize| shared.get(k).copied().filter(|&g| g < files.len()).unwrap_or(k);
313    let mut own: Vec<u32> = files.iter().map(|f| f.1.unwrap_or(0)).collect();
314    for &r in &roots {
315        if let Some(k) = items[r].file {
316            own[k as usize] = own[k as usize].max(items[r].size);
317        }
318    }
319    let mut area_size = vec![0u32; files.len()];
320    for (k, &size) in own.iter().enumerate() {
321        area_size[owner(k)] = area_size[owner(k)].max(size);
322    }
323    let mut cursor = 0u32;
324    let mut root_offsets: Vec<(String, u32)> = Vec::new();
325    let mut area_starts: Vec<Option<u32>> = vec![None; files.len()];
326    for &r in &roots {
327        if items[r].local {
328            continue;
329        }
330        if items[r].linkage.is_some() {
331            place(&mut items, r, 0, Vec::new());
332            continue;
333        }
334        if let Some(k) = items[r].file {
335            let g = owner(k as usize);
336            let start = *area_starts[g].get_or_insert(cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT);
337            cursor = cursor.max(start + area_size[g]);
338            if cursor > MAX_STORAGE {
339                return Err(Error::at(items[r].pos, format!("storage exceeds the interpreter's {MAX_STORAGE} bytes")));
340            }
341            place(&mut items, r, start, Vec::new());
342            continue;
343        }
344        let offset = match &items[r].redefines {
345            Some(target) => root_offsets.iter().find(|(n, _)| n == target).map(|&(_, o)| o).ok_or_else(|| {
346                Error::at(items[r].pos, format!("REDEFINES {target}: no earlier 01-level item of that name"))
347            })?,
348            None => cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT,
349        };
350        cursor = cursor.max(offset + items[r].size);
351        if cursor > MAX_STORAGE {
352            return Err(Error::at(items[r].pos, format!("WORKING-STORAGE exceeds the interpreter's {MAX_STORAGE} bytes")));
353        }
354        if let Some(name) = &items[r].name {
355            root_offsets.push((name.clone(), offset));
356        }
357        place(&mut items, r, offset, Vec::new());
358    }
359    let tables: Vec<usize> = (0..items.len()).filter(|&i| items[i].depending_on.is_some()).collect();
360    for t in tables {
361        let (mut child, mut at) = (t, items[t].parent);
362        while let Some(a) = at {
363            let last = items[a].children.iter().rev().find(|&&c| items[c].redefines.is_none()).copied();
364            if last != Some(child) {
365                return Err(Error::at(items[t].pos, "items after an OCCURS DEPENDING ON table in the same record are not supported yet"));
366            }
367            items[a].odo.get_or_insert(t);
368            (child, at) = (a, items[a].parent);
369        }
370    }
371    for (index, e) in renames {
372        rename(&mut items, index, e, qualify)?;
373    }
374    let mut areas = Vec::new();
375    for (k, &size) in own.iter().enumerate() {
376        let g = owner(k);
377        let start = match area_starts[g] {
378            Some(start) => start,
379            None => {
380                let start = cursor.div_ceil(LEVEL_ALIGNMENT) * LEVEL_ALIGNMENT;
381                area_starts[g] = Some(start);
382                cursor = cursor.max(start + area_size[g]);
383                start
384            }
385        };
386        areas.push((start, size));
387    }
388    let mut record_lengths: Vec<Option<(u32, u32)>> = vec![None; files.len()];
389    for &r in &roots {
390        let Some(k) = items[r].file else { continue };
391        let fewer = items[r].odo.map_or(0, |t| {
392            let t = &items[t];
393            let outer: u32 = t.dims[..t.dims.len().saturating_sub(1)].iter().map(|&(_, n)| n).product();
394            t.occurs.saturating_sub(t.occurs_min) * t.size * outer
395        });
396        let (least, most) = (items[r].size.saturating_sub(fewer), items[r].size);
397        let lengths = &mut record_lengths[k as usize];
398        *lengths = Some(lengths.map_or((least, most), |(l, m)| (l.min(least), m.max(most))));
399    }
400    Ok(Layout { items, conditions, edits, currencies, file_areas: areas, record_lengths, linkage_roots, local_size: local_cursor, size: cursor, file_names: Vec::new(), linage_counters: Vec::new(), qualify })
401}
402
403/// The names of item `start` and each group above it, nearest first: the hierarchy of names that
404/// qualifies an item `start` holds or a condition-name of `start`. FILLER and unnamed items give
405/// none.
406fn names_from(items: &[Item], start: Option<usize>) -> impl Iterator<Item = &str> {
407    std::iter::successors(start, |&p| items[p].parent).filter_map(|p| items[p].name.as_deref())
408}
409
410/// Gives level-66 entry `index` the storage and attributes of what it renames (Language Reference
411/// SC27-8713-03, pp. 228-229): one item as that item is, or from the start of the first item
412/// through the end of the last as an alphanumeric group.
413fn rename(items: &mut [Item], index: usize, e: &DataEntry, qualify: Qualify) -> Result<(), Error> {
414    let Some((first, last)) = &e.renames else { return Ok(()) };
415    let record = items[index].parent.unwrap_or(index);
416    let find = |r: &Ref| -> Result<usize, Error> {
417        let err = |m: String| Err(Error::at(r.pos, format!("RENAMES {}: {m}", r.name)));
418        if !r.subscripts.is_empty() || r.refmod.is_some() {
419            return err("a renamed item is named without subscripts or reference modification".into());
420        }
421        let in_record = |mut at: usize| {
422            let mut wanted = r.qualifiers.iter().peekable();
423            while let Some(p) = items[at].parent {
424                if wanted.peek().is_some_and(|q| items[p].name.as_deref() == Some(q.as_str())) {
425                    wanted.next();
426                }
427                at = p;
428            }
429            at == record && wanted.next().is_none()
430        };
431        let mut found: Vec<usize> = (0..items.len()).filter(|&i| i != record && items[i].level != 66 && items[i].name.as_deref() == Some(r.name.as_str()) && in_record(i)).collect();
432        if found.len() > 1 && qualify == Qualify::Extend {
433            let complete: Vec<usize> = found.iter().copied().filter(|&i| names_from(items, items[i].parent).eq(r.qualifiers.iter().map(String::as_str))).collect();
434            if complete.len() == 1 {
435                found = complete;
436            }
437        }
438        let &[t] = found.as_slice() else {
439            let record_name = items[record].name.clone().unwrap_or_default();
440            return err(if found.is_empty() { format!("no item of that name below {record_name}, other than a level-66 entry") } else { "ambiguous; qualify it with OF or IN".into() });
441        };
442        if !items[t].dims.is_empty() {
443            return err("a renamed item must not have OCCURS, nor belong to a group that has it".into());
444        }
445        Ok(t)
446    };
447    if first.name == items[record].name.clone().unwrap_or_default() && first.qualifiers.is_empty() {
448        return Err(Error::at(first.pos, format!("RENAMES {}: a level-66 entry cannot rename a level-01 record", first.name)));
449    }
450    let a = find(first)?;
451    let Some(last) = last else {
452        let (offset, size, kind, scaling, odo) = (items[a].offset, items[a].size, items[a].kind, items[a].scaling, items[a].odo);
453        let it = &mut items[index];
454        (it.offset, it.size, it.kind, it.scaling, it.odo) = (offset, size, kind, scaling, odo);
455        return Ok(());
456    };
457    let b = find(last)?;
458    let end = |i: usize| items[i].offset + items[i].size;
459    let mut up = items[b].parent;
460    while let Some(p) = up {
461        if p == a {
462            return Err(Error::at(last.pos, format!("RENAMES {} THRU {}: the last item cannot be within the first", first.name, last.name)));
463        }
464        up = items[p].parent;
465    }
466    if a == b || items[b].offset < items[a].offset || end(b) < end(a) {
467        return Err(Error::at(last.pos, format!("RENAMES {} THRU {}: the last item must start and end no earlier than the first", first.name, last.name)));
468    }
469    let root = |mut i: usize| {
470        while let Some(p) = items[i].parent {
471            i = p;
472        }
473        i
474    };
475    if let Some(t) = (0..items.len()).find(|&i| items[i].depending_on.is_some() && root(i) == record && items[i].offset >= items[a].offset && items[i].offset < end(b)) {
476        return Err(Error::at(items[t].pos, format!("RENAMES {} THRU {}: no OCCURS DEPENDING ON between them", first.name, last.name)));
477    }
478    let (offset, size) = (items[a].offset, end(b) - items[a].offset);
479    let it = &mut items[index];
480    (it.offset, it.size, it.kind) = (offset, size, Kind::Group);
481    Ok(())
482}
483
484/// The boundary a SYNCHRONIZED item of this kind is aligned on (Language Reference SC27-8713-03,
485/// pp. 232-233); 1 for the kinds SYNCHRONIZED leaves where they are.
486fn alignment(kind: Kind) -> u32 {
487    match kind {
488        Kind::Binary { digits: 0..=4, .. } => 2,
489        Kind::Binary { .. } | Kind::Float(Precision::Short) | Kind::Pointer | Kind::Index | Kind::ObjectReference | Kind::ProgramPointer => 4,
490        Kind::Float(_) => 8,
491        _ => 1,
492    }
493}
494
495fn kind(e: &DataEntry, item: &Item, usage: Option<Usage>, pic: Option<&picture::Picture>, edits: &mut Vec<Vec<Sym>>) -> Result<Kind, Error> {
496    let err = |m: String| Error::at(e.pos, m);
497    let usage = usage.unwrap_or_default();
498    let handle = matches!(usage, Usage::ObjectReference | Usage::ProgramPointer);
499    let elementary = e.picture.is_some() || (handle || matches!(usage, Usage::Float1 | Usage::Float2 | Usage::Pointer | Usage::Index)) && item.children.is_empty();
500    if !elementary {
501        return if item.children.is_empty() { Err(err("an elementary item needs a PICTURE".into())) } else { Ok(Kind::Group) };
502    }
503    if !item.children.is_empty() {
504        return Err(err("a group item cannot have a PICTURE".into()));
505    }
506    if handle {
507        if e.picture.is_some() || e.value.as_ref().is_some_and(|v| *v != Literal::Figurative(syntax::ast::Figurative::Null)) {
508            return Err(err("an object reference, function-pointer or procedure-pointer takes no PICTURE and only VALUE NULL".into()));
509        }
510        return Ok(if usage == Usage::ObjectReference { Kind::ObjectReference } else { Kind::ProgramPointer });
511    }
512    if let Usage::Pointer | Usage::Index = usage {
513        if e.picture.is_some() {
514            return Err(err("POINTER and INDEX items take no PICTURE".into()));
515        }
516        return Ok(if usage == Usage::Pointer { Kind::Pointer } else { Kind::Index });
517    }
518    if let Usage::Float1 | Usage::Float2 = usage {
519        if e.picture.is_some() {
520            return Err(err("COMP-1 and COMP-2 items take no PICTURE".into()));
521        }
522        return Ok(Kind::Float(if usage == Usage::Float1 { Precision::Short } else { Precision::Long }));
523    }
524    let Some(pic) = pic else { return Err(err("an elementary item needs a PICTURE".into())) };
525    let blank_numeric;
526    let pic = match pic.category {
527        Category::Numeric if e.blank_when_zero && usage == Usage::Display => {
528            blank_numeric = picture::blank_when_zero(pic).map_err(err)?;
529            &blank_numeric
530        }
531        Category::NumericEdited => pic,
532        Category::Numeric if e.blank_when_zero && usage == Usage::National => return Err(err("BLANK WHEN ZERO on a USAGE NATIONAL item is not supported yet".into())),
533        _ if e.blank_when_zero => return Err(err("BLANK WHEN ZERO needs a numeric or numeric-edited item of USAGE DISPLAY or NATIONAL".into())),
534        _ => pic,
535    };
536    let k = match (pic.category, usage) {
537        (Category::NumericEdited, Usage::Display) => {
538            edits.push(pic.edit.clone().unwrap_or_default());
539            Kind::NumericEdited { edit: edits.len() as u32 - 1, digits: pic.digits, scale: pic.scale, blank_when_zero: e.blank_when_zero }
540        }
541        (Category::AlphanumericEdited, Usage::Display) => {
542            edits.push(pic.edit.clone().unwrap_or_default());
543            Kind::AlnumEdited { edit: edits.len() as u32 - 1 }
544        }
545        (Category::Numeric, Usage::Display) => Kind::Zoned { digits: pic.digits, scale: pic.scale, signed: pic.signed, sign: e.sign },
546        (Category::Numeric, Usage::Packed) => Kind::Packed { digits: pic.digits, scale: pic.scale, signed: pic.signed },
547        (Category::Numeric, Usage::Binary | Usage::NativeBinary) if pic.digits <= 18 => {
548            Kind::Binary { digits: pic.digits, scale: pic.scale, signed: pic.signed, native: usage == Usage::NativeBinary }
549        }
550        (Category::Numeric, Usage::Binary | Usage::NativeBinary) => return Err(err("a binary item holds at most 18 digits".into())),
551        (Category::Alphanumeric, Usage::Display) => Kind::Alnum { justified: e.justified },
552        (Category::National, Usage::Display | Usage::National) => Kind::National,
553        (category, usage) => return Err(err(format!("a {category:?} PICTURE with USAGE {usage:?} is not supported yet"))),
554    };
555    if let Kind::Zoned { sign: Some(_), signed: false, .. } = k {
556        return Err(err("a SIGN clause needs an S in the PICTURE".into()));
557    }
558    Ok(k)
559}
560
561fn elementary_size(item: &Item, e_size: Option<u32>) -> u32 {
562    match item.kind {
563        Kind::Group => 0,
564        Kind::Alnum { .. } | Kind::NumericEdited { .. } | Kind::AlnumEdited { .. } => e_size.unwrap_or(0),
565        Kind::National => 2 * e_size.unwrap_or(0),
566        Kind::Zoned { digits, sign, .. } => digits + sign.is_some_and(|s| s.separate) as u32,
567        Kind::Packed { digits, .. } => digits / 2 + 1,
568        Kind::Binary { digits, .. } => match digits {
569            0..=4 => 2,
570            5..=9 => 4,
571            _ => 8,
572        },
573        Kind::Float(p) => p.bytes() as u32,
574        Kind::Pointer | Kind::Index | Kind::ObjectReference | Kind::ProgramPointer => 4,
575    }
576}
577
578/// Sizes the item and its descendants; children's offsets are relative to their parent here.
579/// `base` is where the item starts in its record, the 01 level on a doubleword, which is what a
580/// SYNCHRONIZED item's boundary is reckoned from (Language Reference SC27-8713-03, pp. 233-235).
581fn measure(items: &mut [Item], aligns: &[u32], index: usize, base: u32) -> Result<(), Error> {
582    if items[index].kind != Kind::Group {
583        return Ok(());
584    }
585    let children = items[index].children.clone();
586    let (mut cursor, mut extent) = (0u32, 0u32);
587    let mut placed: Vec<(Option<String>, u32)> = Vec::new();
588    let mut previous: Option<usize> = None;
589    for c in children {
590        let redefined = match items[c].redefines.clone() {
591            Some(target) => Some(placed.iter().rev().find(|(n, _)| n.as_deref() == Some(target.as_str())).map(|&(_, o)| o).ok_or_else(|| {
592                Error::at(items[c].pos, format!("REDEFINES {target}: no earlier item of that name at this level"))
593            })?),
594            None => None,
595        };
596        let start = redefined.unwrap_or(cursor);
597        let m = first_alignment(items, aligns, c);
598        let slack = (m - (base + start) % m) % m;
599        if slack > 0 && redefined.is_some() {
600            return Err(Error::at(items[c].pos, format!("a SYNCHRONIZED item at the start of a REDEFINES would need {slack} slack bytes: the redefined item must be on a {m}-byte boundary")));
601        }
602        if slack > 0 {
603            give_slack(items, previous, cursor, slack);
604        }
605        let offset = start + slack;
606        measure(items, aligns, c, base + offset)?;
607        if items[c].table {
608            let m = widest_alignment(items, aligns, c);
609            items[c].size = items[c].size.div_ceil(m) * m;
610        }
611        let too_large = || Error::at(items[c].pos, format!("an item larger than the interpreter's {MAX_STORAGE} bytes"));
612        let span = items[c].size.checked_mul(items[c].occurs).filter(|&s| s <= MAX_STORAGE).ok_or_else(too_large)?;
613        if redefined.is_none() {
614            cursor = offset.checked_add(span).filter(|&s| s <= MAX_STORAGE).ok_or_else(too_large)?;
615        }
616        extent = extent.max(offset + span);
617        items[c].offset = offset;
618        placed.push((items[c].name.clone(), offset));
619        previous = Some(c);
620    }
621    items[index].size = cursor.max(extent);
622    Ok(())
623}
624
625/// The boundary the first elementary item within `index` is aligned on.
626fn first_alignment(items: &[Item], aligns: &[u32], index: usize) -> u32 {
627    match items[index].children.first() {
628        Some(&c) if items[index].kind == Kind::Group => first_alignment(items, aligns, c),
629        _ => aligns[index],
630    }
631}
632
633/// The widest boundary of any elementary item within `index`, to which each occurrence of a table
634/// is padded so that every occurrence aligns as the first does.
635fn widest_alignment(items: &[Item], aligns: &[u32], index: usize) -> u32 {
636    items[index].children.iter().map(|&c| widest_alignment(items, aligns, c)).fold(aligns[index], u32::max)
637}
638
639/// Slack bytes before a SYNCHRONIZED item belong at the level of the elementary item before it
640/// (Language Reference SC27-8713-03, p. 234): so they lengthen the group sibling that ends where
641/// they start, and that group's last group, down to the elementary item's own group. A table or
642/// a redefinition keeps its length and leaves them to the group it is in.
643fn give_slack(items: &mut [Item], previous: Option<usize>, cursor: u32, slack: u32) {
644    let Some(mut g) = previous else { return };
645    let mut end = cursor;
646    while items[g].kind == Kind::Group && !items[g].table && items[g].redefines.is_none() && items[g].offset + items[g].size == end {
647        end = items[g].size;
648        items[g].size += slack;
649        match items[g].children.last() {
650            Some(&c) => g = c,
651            None => return,
652        }
653    }
654}
655
656fn place(items: &mut [Item], index: usize, offset: u32, mut dims: Vec<(u32, u32)>) {
657    items[index].offset = offset;
658    if items[index].table {
659        dims.push((items[index].size, items[index].occurs));
660    }
661    items[index].dims = dims.clone();
662    for c in items[index].children.clone() {
663        let relative = items[c].offset;
664        place(items, c, offset + relative, dims.clone());
665    }
666}
667
668impl Layout {
669    /// Names the files, so that a file-name qualifies its records and its LINAGE-COUNTER.
670    pub fn name_files(&mut self, files: &[FileDecl], linage_counters: Vec<Option<usize>>) {
671        self.file_names = files.iter().map(|f| f.name.clone()).collect();
672        self.linage_counters = linage_counters;
673    }
674
675    /// The file whose name qualifies item `i`: the file of its record, or the one it is the
676    /// LINAGE-COUNTER of.
677    fn file_qualifying(&self, mut i: usize) -> Option<&str> {
678        while let Some(p) = self.items[i].parent {
679            i = p;
680        }
681        let k = self.items[i].file.map(usize::from).or_else(|| self.linage_counters.iter().position(|&c| c == Some(i)))?;
682        self.file_names.get(k).map(String::as_str)
683    }
684
685    /// Whether `qualifiers` are the complete set of a candidate: every name of its hierarchy, the
686    /// file-name of its FD or SD allowed last but not needed (assumption
687    /// [`numeric::assumptions::COMPLETE_SET_OF_QUALIFIERS`]).
688    fn complete(&self, candidate: Resolved, qualifiers: &[String]) -> bool {
689        let (start, own) = match candidate {
690            Resolved::Item(i) => (self.items[i].parent, i),
691            Resolved::Condition(c) => (Some(self.conditions[c].item), self.conditions[c].item),
692        };
693        let given = || qualifiers.iter().map(String::as_str);
694        given().eq(names_from(&self.items, start)) || given().eq(names_from(&self.items, start).chain(self.file_qualifying(own)))
695    }
696
697    /// A data item, condition-name or LINAGE-COUNTER named with its qualifiers, the last of which
698    /// may be the file-name of an FD or SD (Language Reference SC27-8713-03, pp. 69-70). Under
699    /// QUALIFY(EXTEND) a reference the standard's rules find ambiguous names the one candidate it
700    /// gives a complete set of qualifiers, if only one (Programming Guide SC27-8714-03, p. 400;
701    /// Language Reference SC27-8713-03, pp. 67-68).
702    pub fn resolve(&self, name: &str, qualifiers: &[String], pos: Pos) -> Result<Resolved, Error> {
703        let within = |mut at: Option<usize>, own: usize| {
704            let mut wanted = qualifiers.iter();
705            let mut next = wanted.next();
706            while let (Some(q), Some(i)) = (next, at) {
707                if self.items[i].name.as_deref() == Some(q.as_str()) {
708                    next = wanted.next();
709                }
710                at = self.items[i].parent;
711            }
712            match next {
713                None => true,
714                Some(file) => wanted.next().is_none() && self.file_qualifying(own) == Some(file.as_str()),
715            }
716        };
717        let mut found: Vec<Resolved> = self
718            .items
719            .iter()
720            .enumerate()
721            .filter(|(i, it)| it.name.as_deref() == Some(name) && within(it.parent, *i))
722            .map(|(i, _)| Resolved::Item(i))
723            .collect();
724        found.extend(
725            self.conditions.iter().enumerate().filter(|(_, c)| c.name == name && within(Some(c.item), c.item)).map(|(i, _)| Resolved::Condition(i)),
726        );
727        if found.len() > 1 && self.qualify == Qualify::Extend {
728            let complete: Vec<Resolved> = found.iter().copied().filter(|&r| self.complete(r, qualifiers)).collect();
729            if let [one] = complete.as_slice() {
730                return Ok(*one);
731            }
732        }
733        match found.as_slice() {
734            [one] => Ok(*one),
735            [] => Err(Error::at(pos, format!("{name} is not defined"))),
736            _ => Err(Error::at(pos, format!("{name} is ambiguous; qualify it with OF or IN"))),
737        }
738    }
739}
740
741/// The file whose record area each file uses: its own, or the first file of its SAME RECORD AREA
742/// clause. SAME AREA shares the record area of the VSAM (indexed and relative) files it names and
743/// is documentation for the rest: SAME_AREA_VSAM in numeric::assumptions.
744pub fn record_area_owners(files: &[FileDecl], environment: &Environment) -> Result<Vec<usize>, Error> {
745    let mut owner: Vec<usize> = (0..files.len()).collect();
746    let root = |owner: &[usize], mut k: usize| {
747        while owner[k] != k {
748            k = owner[k];
749        }
750        k
751    };
752    let clauses = environment.same_record_areas.iter().map(|c| ("SAME RECORD AREA", c)).chain(environment.same_areas.iter().map(|c| ("SAME AREA", c)));
753    for (clause, names) in clauses {
754        let mut members = Vec::new();
755        for name in names {
756            let k = files.iter().position(|f| f.name == *name).ok_or_else(|| Error::at(Pos::default(), format!("{clause} names {name}, which is not a file")))?;
757            if clause == "SAME RECORD AREA" || matches!(files[k].organization, Organization::Indexed | Organization::Relative) {
758                members.push(k);
759            }
760        }
761        for pair in members.windows(2) {
762            let (a, b) = (root(&owner, pair[0]), root(&owner, pair[1]));
763            owner[a.max(b)] = a.min(b);
764        }
765    }
766    Ok((0..files.len()).map(|k| root(&owner, k)).collect())
767}