Skip to main content

ironwork_rt/sort/
run.rs

1//! A program's SORT and MERGE (lir.md ยง9.6). Records are held in memory: the input phase gathers
2//! them from USING files or RELEASE, a stable sort orders them, and the output phase hands them to
3//! GIVING files or RETURN. SORT-RETURN reports how the statement ended. A table SORT reorders the
4//! table's elements in place.
5
6use super::keys::{Collating, Format, KeyValue, decimal, order};
7use crate::abend::{Abend, AbendCode, Ending, Signal};
8use crate::fileio::{self, File, Files};
9use crate::files::{FileStatus, Format as Records, Move, Record};
10use crate::host::{self, Host};
11use crate::lir::{FileSort, Organization, SortIo, SortKeys};
12use crate::storage::{Kind, Loc, Val};
13use crate::store::{self, ProgramFacts};
14use crate::vocab::{OpenMode, Pos};
15use numeric::precision::{Fixed, Places};
16use numeric::{FastsrtAdvPrint, SortKeys as KeyReading, TruncCheck, assumptions};
17use std::cmp::Ordering;
18use std::io::Write;
19use std::rc::Rc;
20use zarch::ebcdic;
21
22type R<T> = Result<T, Abend>;
23
24/// A key's place in the record (or table element) and how the program reads it.
25#[derive(Clone, Debug)]
26pub struct ItemKey {
27    pub ascending: bool,
28    pub offset: usize,
29    pub len: usize,
30    pub kind: Kind,
31    pub item: usize,
32    /// The order of an alphanumeric key's characters.
33    pub collating: Collating,
34}
35
36/// The keys a lowered SORT carries.
37pub fn item_keys(keys: &SortKeys) -> Vec<ItemKey> {
38    let collating = keys.collating.as_ref().map_or(Collating::Ebcdic, |p| Collating::Positions(Rc::new(**p)));
39    keys.keys
40        .iter()
41        .map(|k| ItemKey {
42            ascending: k.ascending,
43            offset: k.offset as usize,
44            len: k.len as usize,
45            kind: k.kind,
46            item: k.item as usize,
47            collating: if k.collated { collating.clone() } else { Collating::Ebcdic },
48        })
49        .collect()
50}
51
52/// A USING, GIVING or SD file as SELECT and FD declare it.
53#[derive(Clone, Copy)]
54pub struct SortFile<'a, P, X> {
55    pub file: File<'a, P, X>,
56    /// No RECORDING MODE V, and its smallest record as long as its largest.
57    pub fixed: bool,
58    /// FILE STATUS and RELATIVE KEY as written, which the FASTSRT reports name.
59    pub status_name: Option<&'a str>,
60    pub relative_name: Option<&'a str>,
61}
62
63/// An input or output procedure, as DEBUG-ITEM names how control came to it.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Procedure {
66    SortInput,
67    SortOutput,
68    MergeOutput,
69}
70
71impl Procedure {
72    pub fn name(self) -> &'static str {
73        match self {
74            Procedure::SortInput => "SORT INPUT",
75            Procedure::SortOutput => "SORT OUTPUT",
76            Procedure::MergeOutput => "MERGE OUTPUT",
77        }
78    }
79}
80
81/// What a SORT or MERGE asks of the executor beyond [`Files`]: its handles to the special registers,
82/// procedures, files and keys the statement names, the file statements with their EXCEPTION/ERROR
83/// procedures, running a procedure, and where the sort in progress is kept.
84pub trait SortHost<'a, P: Copy, X: Copy>: Files<P, X> {
85    type Register: Copy;
86    type Procedure: Copy;
87    type File;
88    type Keys;
89    fn locate_register(&mut self, register: Self::Register) -> R<Loc>;
90    fn register_value(&mut self, register: Self::Register, pos: Pos) -> R<i64>;
91    fn file_index(&self, file: &Self::File, pos: Pos) -> R<usize>;
92    fn keys(&mut self, keys: &Self::Keys, pos: Pos) -> R<Vec<ItemKey>>;
93    fn sort_file(&self, k: usize) -> SortFile<'a, P, X>;
94    fn active(&mut self) -> &mut Option<Active>;
95    /// OPEN of file `k`, as the statement runs it.
96    fn open(&mut self, k: usize, mode: OpenMode, pos: Pos) -> R<()>;
97    /// CLOSE of file `k`; true when it failed.
98    fn close(&mut self, k: usize, pos: Pos) -> R<bool>;
99    /// WRITE of the record at `loc` to file `k`, with no phrases; true when it failed.
100    fn put(&mut self, k: usize, loc: Loc, pos: Pos) -> R<bool>;
101    /// Records a status of file `k`, open in `mode` or being opened in it, and takes the file's
102    /// error path when it fails.
103    fn fail(&mut self, k: usize, status: FileStatus, mode: Option<OpenMode>, message: String, pos: Pos) -> R<()>;
104    /// Whether an EXCEPTION/ERROR procedure applies to file `k` in `mode`.
105    fn has_error_procedure(&self, k: usize, mode: OpenMode) -> bool;
106    /// Runs an input or output procedure; the Ending of a STOP RUN or GOBACK in it.
107    fn run_procedure(&mut self, procedure: Self::Procedure, kind: Procedure, pos: Pos) -> R<Option<Ending>>;
108    fn err(&mut self) -> &mut dyn Write;
109}
110
111/// A SORT or MERGE whose input or output procedure is running.
112pub struct Active {
113    sd: usize,
114    keys: Vec<ItemKey>,
115    phase: Phase,
116}
117
118enum Phase {
119    Input(Vec<Entry>),
120    Output { records: Vec<Vec<u8>>, next: usize },
121}
122
123struct Entry {
124    record: Vec<u8>,
125    keys: Vec<KeyValue>,
126}
127
128enum Input {
129    Record(Vec<u8>),
130    End,
131    Failed(String),
132}
133
134/// A USING or GIVING file of a SORT or MERGE, and why FASTSRT cannot give DFSORT its I/O.
135struct Fastsrt {
136    input: bool,
137    file: usize,
138    refusal: Option<String>,
139}
140
141impl Fastsrt {
142    fn dfsort(&self, fastsrt: bool) -> bool {
143        fastsrt && self.refusal.is_none()
144    }
145}
146
147/// The USING file whose data set DFSORT would read as its SORTIN.
148fn sortin(plan: &[Fastsrt]) -> Option<&Fastsrt> {
149    plan.iter().find(|f| f.input && f.refusal.is_none())
150}
151
152/// How a SORT or MERGE ended: Err holds why it failed.
153type Outcome = Result<(), String>;
154
155/// How a procedure ended: a STOP RUN or GOBACK in it, or Err with why RELEASE or RETURN stopped the
156/// sort.
157type Ended = Result<Option<Ending>, String>;
158
159fn stop(why: String, pos: Pos) -> Abend {
160    Abend { code: AbendCode::Signal(Signal::SortStopped), message: why, pos, file: None }
161}
162
163fn stopped_by_program() -> String {
164    format!("SORT-RETURN was set to 16 (see {})", assumptions::SORT_RETURN_STOPS)
165}
166
167fn ascending(keys: &[ItemKey]) -> Vec<bool> {
168    keys.iter().map(|k| k.ascending).collect()
169}
170
171fn set_register<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, register: H::Register, value: i64, pos: Pos) -> R<()> {
172    let dest = x.locate_register(register)?;
173    x.store_fixed(dest, &Fixed::new(value as i128, Places::new(19, 0)), pos)
174}
175
176/// Each key's value, from storage at `base`: a zoned or packed key as DFSORT reads it when
177/// `dfsort`, an alphanumeric key by its collating sequence, every other key as the program would
178/// read it.
179pub fn key_values<P: Copy>(x: &mut impl Host<P>, base: usize, keys: &[ItemKey], dfsort: bool, pos: Pos) -> R<Vec<KeyValue>> {
180    let mut out = Vec::with_capacity(keys.len());
181    for k in keys {
182        let loc = Loc { offset: base + k.offset, len: k.len, kind: k.kind, item: k.item };
183        let bytes = store::bytes(x.mem(), loc);
184        let value = match (dfsort.then(|| Format::of_decimal(k.kind).and_then(|f| decimal(bytes, f))).flatten(), &k.collating) {
185            (Some((negative, digits)), _) => KeyValue::Decimal { negative, digits },
186            (None, Collating::Positions(_)) => KeyValue::Collated(k.collating.collate(bytes)),
187            (None, Collating::Ebcdic) => KeyValue::Read(host::read(x, loc, pos)?),
188        };
189        out.push(value);
190    }
191    Ok(out)
192}
193
194/// Sort control statements would change what DFSORT does, and none are read here.
195fn refuse_control_statements<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, sort_control: H::Register, pos: Pos) -> R<()> {
196    let loc = x.locate_register(sort_control)?;
197    let page = x.facts().page();
198    let dd = page.decode(store::bytes(x.mem(), loc)).trim().to_ascii_uppercase();
199    if !dd.is_empty() && x.dd(&dd).is_some() {
200        return Err(Abend::ironwork(format!("DD {dd} holds sort control statements, which ironwork for COBOL does not read"), pos));
201    }
202    Ok(())
203}
204
205/// A record as it enters the sort: fitted to `extent`, the length of the sort's records (the
206/// SD's, unless DFSORT reads a longer data set), and keyed at the SD's places from its first
207/// byte. See [`assumptions::SORT_RECORD_LENGTHS`] and [`assumptions::FASTSRT_ADV_PRINT`].
208fn entry<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, sd: usize, keys: &[ItemKey], mut record: Vec<u8>, extent: usize, pos: Pos) -> R<Result<Entry, String>> {
209    let f = x.sort_file(sd);
210    let (area, size) = f.file.area;
211    if f.fixed {
212        record.resize(extent, ebcdic::SPACE);
213    } else {
214        record.truncate(extent);
215    }
216    if keys.iter().any(|k| k.offset + k.len > record.len()) {
217        return Ok(Err(format!("a record of {} bytes ends inside a key", record.len())));
218    }
219    let held = record.len().min(size);
220    x.mem()[area..area + held].copy_from_slice(&record[..held]);
221    let dfsort = x.facts().options().sort_keys == KeyReading::Dfsort;
222    let keys = key_values(x, area, keys, dfsort, pos)?;
223    Ok(Ok(Entry { record, keys }))
224}
225
226/// After a USING or GIVING file fails: with an EXCEPTION/ERROR procedure for it, which has run,
227/// the file's processing ends there and the SORT or MERGE goes on unless the procedure set
228/// SORT-RETURN to 16 ([`assumptions::SORT_FILE_DECLARATIVE`]); without one it fails.
229fn after_failure<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, mode: OpenMode, why: String, sort_return: H::Register, pos: Pos) -> R<Outcome> {
230    if !x.has_error_procedure(k, mode) {
231        return Ok(Err(why));
232    }
233    if x.register_value(sort_return, pos)? == 16 {
234        return Ok(Err(stopped_by_program()));
235    }
236    Ok(Ok(()))
237}
238
239fn is_open<P: Copy, X: Copy>(x: &mut impl Files<P, X>, k: usize) -> bool {
240    x.slot(k).is_some()
241}
242
243/// Opens a USING or GIVING file, which must not be open already.
244fn open_for_sort<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, mode: OpenMode, pos: Pos) -> R<Outcome> {
245    let name = x.sort_file(k).file.name;
246    if is_open(x, k) {
247        x.fail(k, FileStatus::AlreadyOpen, Some(mode), format!("{name} is open, and a SORT or MERGE opens it itself"), pos)?;
248        return Ok(Err(format!("{name} is already open (file status {})", FileStatus::AlreadyOpen.as_str())));
249    }
250    x.open(k, mode, pos)?;
251    if is_open(x, k) { Ok(Ok(())) } else { Ok(Err(format!("OPEN {name} failed"))) }
252}
253
254fn close_for_sort<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, pos: Pos) -> R<Outcome> {
255    let name = x.sort_file(k).file.name;
256    if x.close(k, pos)? { Ok(Err(format!("CLOSE {name} failed"))) } else { Ok(Ok(())) }
257}
258
259/// Records an I/O status of file k in the mode it is open in.
260fn io_status<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, status: FileStatus, message: String, pos: Pos) -> R<()> {
261    let mode = x.slot(k).as_ref().map(|f| f.mode);
262    x.fail(k, status, mode, message, pos)
263}
264
265/// The next record of an open USING file, with the file status READ would set. A print file's
266/// record under ADV keeps the byte before it only when `dfsort` reads it
267/// ([`assumptions::FASTSRT_PRINT_RECORDS`]).
268fn next_input<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, dfsort: bool, pos: Pos) -> R<Input> {
269    let file = x.sort_file(k).file;
270    let size = file.area.1;
271    let Some(mut f) = x.slot(k).take() else { return Ok(Input::Failed("the file is not open".into())) };
272    let text = f.format == Records::Text && !f.is_keyed();
273    let added = fileio::adds_control_byte(&file, f.format);
274    let read = match f.keyed() {
275        Some(keyed) => match keyed.step(false) {
276            Ok(Some(found)) => Ok(Record::Data(found.record)),
277            Ok(None) => Ok(Record::End),
278            Err(code) => Err((FileStatus::from(code), "there is no next record".to_owned())),
279        },
280        None => f.read(size + usize::from(added)).map_err(|e| (FileStatus::PermanentError, e.to_string())),
281    };
282    *x.slot(k) = Some(f);
283    let (record, code) = match read {
284        Err((code, message)) => {
285            io_status(x, k, code, format!("{}: {message}", file.name), pos)?;
286            return Ok(Input::Failed(format!("reading {} failed (file status {})", file.name, code.as_str())));
287        }
288        Ok(Record::End) => {
289            fileio::set_status(x, &file, FileStatus::AtEnd, pos)?;
290            return Ok(Input::End);
291        }
292        Ok(Record::Data(r)) => (r, FileStatus::Success),
293        Ok(Record::WrongLength(r)) => (r, FileStatus::SuccessWrongLength),
294    };
295    fileio::set_status(x, &file, code, pos)?;
296    if added && !dfsort {
297        return Ok(Input::Record(record.get(1..).unwrap_or_default().to_vec()));
298    }
299    if !text {
300        return Ok(Input::Record(record));
301    }
302    let page = x.facts().page();
303    let unknown = page.encode_char('?').unwrap_or(0x6F);
304    Ok(Input::Record(String::from_utf8_lossy(&record).chars().map(|c| page.encode_char(c).unwrap_or(unknown)).collect()))
305}
306
307/// What one SORT or MERGE runs with: its SD, keys and FASTSRT plan, and SORT-RETURN.
308struct Run<'k, G> {
309    sd: usize,
310    merge: bool,
311    keys: &'k [ItemKey],
312    sort_return: G,
313    pos: Pos,
314}
315
316/// Reads every USING file to its end, in order. A MERGE's files must each be in the merge order.
317fn gather<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, run: &Run<'_, H::Register>, plan: &[Fastsrt]) -> R<Result<Vec<Entry>, String>> {
318    let mut entries: Vec<Entry> = Vec::new();
319    for f in plan.iter().filter(|f| f.input) {
320        let read = if f.dfsort(x.facts().options().fastsrt) {
321            by_dfsort(x, f.file, |x| read_using(x, run, f.file, true))?
322        } else {
323            read_using(x, run, f.file, false)?
324        };
325        match read {
326            Ok(records) => entries.extend(records),
327            Err(why) => return Ok(Err(why)),
328        }
329    }
330    Ok(Ok(entries))
331}
332
333/// One USING file, opened, read to its end and closed.
334fn read_using<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, run: &Run<'_, H::Register>, k: usize, dfsort: bool) -> R<Result<Vec<Entry>, String>> {
335    let (pos, sd, keys) = (run.pos, run.sd, run.keys);
336    let file = x.sort_file(k).file;
337    let name = file.name;
338    if let Err(why) = open_for_sort(x, k, OpenMode::Input, pos)? {
339        return Ok(after_failure(x, k, OpenMode::Input, why, run.sort_return, pos)?.map(|()| Vec::new()));
340    }
341    let extent = x.sort_file(sd).file.area.1 + if dfsort { control_byte(x, &file) } else { 0 };
342    let mut entries = Vec::new();
343    let mut closed = false;
344    loop {
345        let failure = match next_input(x, k, dfsort, pos)? {
346            Input::End => break,
347            Input::Failed(why) => {
348                x.close(k, pos)?;
349                if let Err(why) = after_failure(x, k, OpenMode::Input, why, run.sort_return, pos)? {
350                    return Ok(Err(why));
351                }
352                closed = true;
353                break;
354            }
355            Input::Record(r) => match entry(x, sd, keys, r, extent, pos)? {
356                Ok(e) => {
357                    entries.push(e);
358                    continue;
359                }
360                Err(why) => format!("{name}: {why}"),
361            },
362        };
363        x.close(k, pos)?;
364        return Ok(Err(failure));
365    }
366    if !closed
367        && let Err(why) = close_for_sort(x, k, pos)?
368        && let Err(why) = after_failure(x, k, OpenMode::Input, why, run.sort_return, pos)?
369    {
370        return Ok(Err(why));
371    }
372    if dfsort && entries.is_empty() && matches!(file.organization, Organization::Indexed | Organization::Relative) {
373        return Ok(Err(format!("{name} is an empty VSAM file, which FASTSRT cannot take as input (see {})", assumptions::FASTSRT_FAILURE)));
374    }
375    let ascending = ascending(keys);
376    if run.merge
377        && let Some(n) = entries.windows(2).position(|w| order(&w[0].keys, &w[1].keys, &ascending) == Ordering::Greater)
378    {
379        return Ok(Err(format!("record {} of {name} is out of the merge order (see {})", n + 2, assumptions::MERGE_OUT_OF_SEQUENCE_FAILS)));
380    }
381    Ok(Ok(entries))
382}
383
384/// Writes every record to each GIVING file, as WRITE without phrases would, or as DFSORT writes
385/// a sequential file's data set.
386fn scatter<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, run: &Run<'_, H::Register>, records: &[Vec<u8>], plan: &[Fastsrt]) -> R<Outcome> {
387    for f in plan.iter().filter(|f| !f.input) {
388        let written = if f.dfsort(x.facts().options().fastsrt) {
389            by_dfsort(x, f.file, |x| write_giving(x, run, records, f.file, true))?
390        } else {
391            write_giving(x, run, records, f.file, false)?
392        };
393        if written.is_err() {
394            return Ok(written);
395        }
396    }
397    Ok(Ok(()))
398}
399
400/// One GIVING file, opened, written and closed. COBOL takes each record at the SD's length at
401/// most ([`assumptions::FASTSRT_ADV_PRINT`]).
402fn write_giving<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, run: &Run<'_, H::Register>, records: &[Vec<u8>], k: usize, dfsort: bool) -> R<Outcome> {
403    let pos = run.pos;
404    let file = x.sort_file(k).file;
405    let name = file.name;
406    if let Err(why) = open_for_sort(x, k, OpenMode::Output, pos)? {
407        return after_failure(x, k, OpenMode::Output, why, run.sort_return, pos);
408    }
409    let (area, size) = file.area;
410    let limit = size.min(x.sort_file(run.sd).file.area.1);
411    for record in records {
412        if dfsort && file.organization == Organization::Sequential {
413            if let Err(why) = dfsort_put(x, k, record)? {
414                x.close(k, pos)?;
415                return Ok(Err(why));
416            }
417            continue;
418        }
419        let len = record.len().min(limit);
420        x.mem()[area..area + len].copy_from_slice(&record[..len]);
421        let loc = Loc { offset: area, len, kind: Kind::Alnum { justified: false }, item: usize::MAX };
422        if x.put(k, loc, pos)? {
423            x.close(k, pos)?;
424            return after_failure(x, k, OpenMode::Output, format!("WRITE {name} failed"), run.sort_return, pos);
425        }
426    }
427    match close_for_sort(x, k, pos)? {
428        Err(why) => after_failure(x, k, OpenMode::Output, why, run.sort_return, pos),
429        closed => Ok(closed),
430    }
431}
432
433/// 1 when the file's data set holds a byte for ADV's printer control character before each
434/// record, which DFSORT reads and writes as part of the record; else 0.
435fn control_byte<P: Copy, X: Copy>(x: &impl Files<P, X>, file: &File<'_, P, X>) -> usize {
436    usize::from(fileio::adds_control_byte(file, fileio::dd_format(x, file)))
437}
438
439/// A record as DFSORT writes it to a sequential GIVING file's data set: with no printer control
440/// character, a fixed-length record padded with X'00' or cut to the data set's length, and a
441/// variable-length one longer than that a failure. A text DD shows each record as a line. See
442/// FASTSRT_PRINT_RECORDS, FASTSRT_RECORD_LENGTHS and FASTSRT_ADV_PRINT in numeric::assumptions.
443fn dfsort_put<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, k: usize, record: &[u8]) -> R<Outcome> {
444    let file = x.sort_file(k).file;
445    let name = file.name;
446    let Some(mut f) = x.slot(k).take() else { return Ok(Err(format!("{name} is not open"))) };
447    let lrecl = file.area.1 + control_byte(x, &file);
448    let written = match f.format {
449        Records::Text => {
450            let line = x.facts().page().decode(record).trim_end().to_owned();
451            f.print(Some(Move::Lines(1)), &line, None)
452        }
453        Records::Fixed => {
454            let mut bytes = record.to_vec();
455            bytes.resize(lrecl, 0x00);
456            f.write(&bytes)
457        }
458        Records::Variable if record.len() > lrecl => {
459            *x.slot(k) = Some(f);
460            let why = format!("a {}-byte record is longer than the largest of GIVING {name}'s data set, {lrecl} bytes: ICE217A (see {})", record.len(), assumptions::FASTSRT_RECORD_LENGTHS);
461            return Ok(Err(why));
462        }
463        Records::Variable => f.write(record),
464    };
465    *x.slot(k) = Some(f);
466    Ok(written.map_err(|e| format!("writing {name} failed: {e}")))
467}
468
469/// DFSORT's check of its data sets before it sorts: a fixed-length GIVING data set whose records
470/// are longer than the SD's, with no USING data set of DFSORT's own to pad from, fails the SORT
471/// ([`assumptions::FASTSRT_ADV_PRINT`]).
472fn dfsort_refuses<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &H, sd: usize, plan: &[Fastsrt]) -> Option<String> {
473    if !x.facts().options().fastsrt || sortin(plan).is_some() {
474        return None;
475    }
476    let length = x.sort_file(sd).file.area.1;
477    plan.iter().filter(|f| !f.input && f.refusal.is_none() && fileio::dd_format(x, &x.sort_file(f.file).file) == Records::Fixed).find_map(|f| {
478        let file = x.sort_file(f.file).file;
479        let lrecl = file.area.1 + control_byte(x, &file);
480        (lrecl > length).then(|| {
481            format!("GIVING {}'s data set has {lrecl}-byte records, and with no USING file of its own DFSORT does not pad the SD's {length}-byte records: ICE043A (see {})", file.name, assumptions::FASTSRT_ADV_PRINT)
482        })
483    })
484}
485
486/// Runs `op` on file k as DFSORT does its I/O under FASTSRT: the file's FILE STATUS and RELATIVE
487/// KEY keep the values they had, and a failure fails the sort rather than the run. See
488/// FASTSRT_STATUS and FASTSRT_FAILURE in numeric::assumptions.
489fn by_dfsort<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>, T>(x: &mut H, k: usize, op: impl FnOnce(&mut H) -> R<Result<T, String>>) -> R<Result<T, String>> {
490    let file = x.sort_file(k).file;
491    let mut kept = Vec::new();
492    for p in file.status.into_iter().chain(file.relative) {
493        let loc = x.locate(p, false)?;
494        kept.push((loc, store::bytes(x.mem(), loc).to_vec()));
495    }
496    let was_open = is_open(x, k);
497    let result = match op(x) {
498        Err(a) if matches!(a.code, AbendCode::Io(_)) => Ok(Err(a.message)),
499        other => other,
500    };
501    if !was_open && let Some(f) = x.slot(k).take() {
502        let _ = f.close();
503    }
504    for (loc, bytes) in kept {
505        store::write(x.mem(), loc, &bytes);
506    }
507    result
508}
509
510/// Each USING and GIVING file, and whether IBM's rules let FASTSRT give DFSORT its I/O
511/// (FASTSRT_FILES in numeric::assumptions).
512fn fastsrt_plan<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &H, sort: &FileSort<H::Register, H::Procedure, H::Keys, H::File>, sd: usize, pos: Pos) -> R<Vec<Fastsrt>> {
513    let mut plan = Vec::new();
514    for (input, io) in [(true, &sort.input), (false, &sort.output)] {
515        let Some(SortIo::Files(names)) = io else { continue };
516        for name in names {
517            let k = x.file_index(name, pos)?;
518            let refusal = fastsrt_refusal(x, sort.merge, sd, k, input, names.len(), &plan);
519            plan.push(Fastsrt { input, file: k, refusal });
520        }
521    }
522    Ok(plan)
523}
524
525fn fastsrt_refusal<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &H, merge: bool, sd: usize, k: usize, input: bool, count: usize, earlier: &[Fastsrt]) -> Option<String> {
526    let (this, the_sd) = (x.sort_file(k), x.sort_file(sd));
527    let decl = this.file;
528    let format = |f: &SortFile<'_, P, X>| if f.fixed { "fixed" } else { "variable" };
529    let dfsort_reads = |f: &Fastsrt| f.input && f.refusal.is_none() && (f.file == k || x.sort_file(f.file).file.assign == decl.assign);
530    let options = x.facts().options();
531    Some(if merge {
532        "it applies only to SORT".into()
533    } else if count > 1 {
534        format!("{} names more than one file", if input { "USING" } else { "GIVING" })
535    } else if decl.organization == Organization::LineSequential {
536        "it is a line-sequential file".into()
537    } else if decl.organization == Organization::Relative && !this.fixed {
538        "it is a variable-length relative file".into()
539    } else if !input && decl.linage.is_some() {
540        "its FD has LINAGE".into()
541    } else if x.has_error_procedure(k, if input { OpenMode::Input } else { OpenMode::Output }) {
542        "an EXCEPTION/ERROR procedure applies to it".into()
543    } else if decl.carriage.is_some_and(|c| !c.reserved) && options.fastsrt_adv_print == FastsrtAdvPrint::Exclude {
544        format!("it is a print file, whose records ADV makes a byte longer than its FD's {} ({})", decl.area.1, FastsrtAdvPrint::Exclude.flag())
545    } else if this.fixed != the_sd.fixed {
546        format!("its records are {}-length and the SD's {}-length", format(&this), format(&the_sd))
547    } else if decl.area.1 != the_sd.file.area.1 {
548        format!("its largest record is {} bytes and the SD's {}", decl.area.1, the_sd.file.area.1)
549    } else if !input && earlier.iter().any(dfsort_reads) {
550        "it is also the USING file, whose I/O DFSORT does".into()
551    } else {
552        return None;
553    })
554}
555
556/// Checked mode: where FASTSRT changes what the program sees of a SORT, and where FASTSRT was
557/// asked for and cannot apply.
558fn report_fastsrt<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, merge: bool, sd: usize, plan: &[Fastsrt], pos: Pos) {
559    let options = x.facts().options();
560    if options.trunc_check == TruncCheck::Silent {
561        return;
562    }
563    let verb = if merge { "MERGE" } else { "SORT" };
564    let fastsrt = options.fastsrt;
565    let mut reports = Vec::new();
566    for f in plan {
567        let this = x.sort_file(f.file);
568        let name = this.file.name;
569        let phrase = if f.input { "USING" } else { "GIVING" };
570        match &f.refusal {
571            Some(why) if fastsrt => reports.push(format!("FASTSRT does not apply to {phrase} {name}: {why}; COBOL does its I/O")),
572            Some(_) => {}
573            None => {
574                if let Some(records) = dfsort_records(x, f, sd, plan) {
575                    let does = if fastsrt { "FASTSRT: DFSORT does" } else { "under FASTSRT, DFSORT would do" };
576                    reports.push(format!("{does} the I/O of {phrase} {name}{records}"));
577                }
578                let mut kept: Vec<String> = this.status_name.iter().map(|r| format!("FILE STATUS {r}")).collect();
579                if !f.input && this.file.organization == Organization::Relative {
580                    kept.extend(this.relative_name.iter().map(|r| format!("RELATIVE KEY {r}")));
581                }
582                if kept.is_empty() {
583                    continue;
584                }
585                let kept = kept.join(" and ");
586                reports.push(if fastsrt {
587                    format!("FASTSRT: DFSORT does the I/O of {phrase} {name}, so its {kept} is not updated by the {verb}")
588                } else {
589                    format!("under FASTSRT, DFSORT would do the I/O of {phrase} {name} and its {kept} would not be updated by the {verb}")
590                });
591            }
592        }
593    }
594    let sd_name = x.sort_file(sd).file.name;
595    for report in reports {
596        let _ = writeln!(x.err(), "ironwork: {pos}: {verb} {sd_name}: {report} (-silent stops these reports)");
597    }
598}
599
600/// How DFSORT's records for file f differ from COBOL's: a print file's control character, and
601/// a GIVING data set's record length against the sort's. None when they do not.
602fn dfsort_records<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &H, f: &Fastsrt, sd: usize, plan: &[Fastsrt]) -> Option<String> {
603    let length = x.sort_file(sd).file.area.1;
604    let file = x.sort_file(f.file).file;
605    let byte = control_byte(x, &file);
606    let choice = x.facts().options().fastsrt_adv_print.flag();
607    let mut notes = Vec::new();
608    match file.carriage {
609        Some(c) if c.reserved && !f.input => notes.push(", a print file under NOADV, whose records DFSORT writes as the SD holds them, with no printer control character".to_owned()),
610        Some(c) if !c.reserved && byte == 1 && f.input => notes.push(format!(
611            ", a print file under ADV taken by {choice}, whose records DFSORT reads as its data set holds them, {} bytes with the printer control character first, so each key is read a byte before where the FD has it",
612            length + 1
613        )),
614        Some(c) if !c.reserved && byte == 1 => notes.push(format!(", a print file under ADV taken by {choice}, whose records DFSORT writes with no printer control character")),
615        _ => {}
616    }
617    if !f.input && fileio::dd_format(x, &file) == Records::Fixed && file.organization == Organization::Sequential {
618        let lrecl = file.area.1 + byte;
619        let sorted = sortin(plan).map(|u| length + control_byte(x, &x.sort_file(u.file).file));
620        match sorted {
621            Some(r) if lrecl > r => notes.push(format!("; DFSORT pads each record with X'00' to the data set's {lrecl} bytes (ICE171I)")),
622            Some(r) if lrecl < r => notes.push(format!("; DFSORT cuts each {r}-byte record to the data set's {lrecl} bytes (ICE171I)")),
623            _ => {}
624        }
625    }
626    (!notes.is_empty()).then(|| notes.concat())
627}
628
629/// Runs an input or output procedure with `active` in progress. An Err outcome is a stop
630/// signalled by RELEASE or RETURN.
631fn run_sort_procedure<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, procedure: H::Procedure, active: Active, kind: Procedure, pos: Pos) -> R<(Ended, Option<Active>)> {
632    *x.active() = Some(active);
633    let ended = x.run_procedure(procedure, kind, pos);
634    let active = x.active().take();
635    match ended {
636        Err(a) if a.code == AbendCode::Signal(Signal::SortStopped) => Ok((Err(a.message), active)),
637        Err(a) => Err(a),
638        Ok(ended) => Ok((Ok(ended), active)),
639    }
640}
641
642/// A SORT or MERGE of a file. A failure is reported as DFSORT reports one on SYSOUT, with
643/// SORT-RETURN 16, and the run goes on; a STOP RUN or GOBACK in a procedure ends it.
644pub fn sort<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, plan: &FileSort<H::Register, H::Procedure, H::Keys, H::File>, pos: Pos) -> R<Option<Ending>> {
645    crate::host::unfollowed(x, "SORT and MERGE");
646    let sd = usize::from(plan.sd);
647    let name = x.sort_file(sd).file.name;
648    let verb = if plan.merge { "MERGE" } else { "SORT" };
649    if x.active().is_some() {
650        return Err(Abend::ironwork(format!("{verb} {name}: another SORT or MERGE is in progress"), pos));
651    }
652    refuse_control_statements(x, plan.sort_control, pos)?;
653    set_register(x, plan.sort_return, 0, pos)?;
654    let keys = x.keys(&plan.keys, pos)?;
655    let fastsrt = fastsrt_plan(x, plan, sd, pos)?;
656    report_fastsrt(x, plan.merge, sd, &fastsrt, pos);
657    let end = |x: &mut H, outcome| sort_end(x, plan.merge, name, plan.sort_return, outcome, pos);
658    if let Some(why) = dfsort_refuses(x, sd, &fastsrt) {
659        return end(x, Err(why));
660    }
661    let run = Run { sd, merge: plan.merge, keys: &keys, sort_return: plan.sort_return, pos };
662    let mut entries = match &plan.input {
663        Some(SortIo::Files(_)) => match gather(x, &run, &fastsrt)? {
664            Ok(entries) => entries,
665            Err(why) => return end(x, Err(why)),
666        },
667        Some(SortIo::Procedure(procedure)) => {
668            let active = Active { sd, keys: keys.clone(), phase: Phase::Input(Vec::new()) };
669            let (ended, active) = run_sort_procedure(x, *procedure, active, Procedure::SortInput, pos)?;
670            match ended {
671                Err(why) => return end(x, Err(why)),
672                Ok(Some(e)) => return Ok(Some(e)),
673                Ok(None) => {}
674            }
675            if x.register_value(plan.sort_return, pos)? == 16 {
676                return end(x, Err(stopped_by_program()));
677            }
678            match active.map(|a| a.phase) {
679                Some(Phase::Input(entries)) => entries,
680                _ => Vec::new(),
681            }
682        }
683        None => return Err(Abend::ironwork(format!("{verb} {name}: no input"), pos)),
684    };
685    // A stable sort keeps equal keys in the order they came: SORT_EQUAL_KEYS_IN_ORDER and
686    // MERGE_EQUAL_KEYS_BY_FILE in numeric::assumptions.
687    let up = ascending(&keys);
688    entries.sort_by(|a, b| order(&a.keys, &b.keys, &up));
689    let records: Vec<Vec<u8>> = entries.into_iter().map(|e| e.record).collect();
690    match &plan.output {
691        Some(SortIo::Files(_)) => {
692            let outcome = scatter(x, &run, &records, &fastsrt)?;
693            end(x, outcome)
694        }
695        Some(SortIo::Procedure(procedure)) => {
696            let active = Active { sd, keys: keys.clone(), phase: Phase::Output { records, next: 0 } };
697            let kind = if plan.merge { Procedure::MergeOutput } else { Procedure::SortOutput };
698            match run_sort_procedure(x, *procedure, active, kind, pos)?.0 {
699                Err(why) => end(x, Err(why)),
700                Ok(Some(e)) => Ok(Some(e)),
701                Ok(None) => end(x, Ok(())),
702            }
703        }
704        None => Err(Abend::ironwork(format!("{verb} {name}: no output"), pos)),
705    }
706}
707
708/// Sets SORT-RETURN. A failure is reported as DFSORT reports one on SYSOUT, and the run goes on.
709fn sort_end<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, merge: bool, name: &str, sort_return: H::Register, outcome: Outcome, pos: Pos) -> R<Option<Ending>> {
710    let code = if outcome.is_ok() { 0 } else { 16 };
711    set_register(x, sort_return, code, pos)?;
712    if let Err(why) = outcome {
713        let verb = if merge { "MERGE" } else { "SORT" };
714        let _ = writeln!(x.err(), "ironwork: {pos}: {verb} {name} failed: {why}; SORT-RETURN is 16");
715    }
716    Ok(None)
717}
718
719/// RELEASE's checks, before FROM moves into the record: an input procedure is running, `file` (the
720/// record's) is the one being sorted, and SORT-RETURN does not stop the sort. `name` is the record
721/// as written.
722pub fn release_ready<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, file: Option<usize>, sort_return: H::Register, name: &str, pos: Pos) -> R<()> {
723    crate::host::unfollowed(x, "SORT and MERGE");
724    let Some(Active { sd, phase: Phase::Input(_), .. }) = x.active() else {
725        return Err(Abend::ironwork(format!("RELEASE {name}: no SORT input procedure is running"), pos));
726    };
727    if file != Some(*sd) {
728        return Err(Abend::ironwork(format!("RELEASE {name}: not a record of the file being sorted"), pos));
729    }
730    if x.register_value(sort_return, pos)? == 16 {
731        return Err(stop(stopped_by_program(), pos));
732    }
733    Ok(())
734}
735
736/// RELEASE of the record at `loc`, once [`release_ready`] has passed: a fixed-length SD's whole
737/// record area, else the record.
738pub fn release<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, loc: Loc, name: &str, pos: Pos) -> R<()> {
739    crate::host::unfollowed(x, "SORT and MERGE");
740    let Some(Active { sd, keys, phase: Phase::Input(_) }) = x.active() else {
741        return Err(Abend::ironwork(format!("RELEASE {name}: no SORT input procedure is running"), pos));
742    };
743    let (sd, keys) = (*sd, keys.clone());
744    let f = x.sort_file(sd);
745    let (area, size) = f.file.area;
746    let Ok(loc) = fileio::record_length(x, &f.file, loc, pos)? else {
747        return Err(stop(format!("RELEASE {name}: the record's length is outside its RECORD clause"), pos));
748    };
749    let bytes = if f.fixed { x.mem()[area..area + size].to_vec() } else { store::bytes(x.mem(), loc).to_vec() };
750    match entry(x, sd, &keys, bytes, size, pos)? {
751        Ok(e) => {
752            if let Some(Active { phase: Phase::Input(entries), .. }) = x.active() {
753                entries.push(e);
754            }
755            Ok(())
756        }
757        Err(why) => Err(stop(format!("RELEASE {name}: {why}"), pos)),
758    }
759}
760
761/// RETURN of `file` (`name` as written) into its record area, and moved to `into`: true when a
762/// record came, false at end.
763pub fn return_record<'a, P: Copy, X: Copy, H: SortHost<'a, P, X>>(x: &mut H, file: Option<usize>, into: Option<P>, sort_return: H::Register, name: &str, pos: Pos) -> R<bool> {
764    crate::host::unfollowed(x, "SORT and MERGE");
765    let Some(Active { sd, phase: Phase::Output { .. }, .. }) = x.active() else {
766        return Err(Abend::ironwork(format!("RETURN {name}: no SORT or MERGE output procedure is running"), pos));
767    };
768    let sd = *sd;
769    if file != Some(sd) {
770        return Err(Abend::ironwork(format!("RETURN {name}: not the file being sorted or merged"), pos));
771    }
772    if x.register_value(sort_return, pos)? == 16 {
773        return Err(stop(stopped_by_program(), pos));
774    }
775    let record = match x.active() {
776        Some(Active { phase: Phase::Output { records, next }, .. }) if *next < records.len() => {
777            *next += 1;
778            Some(std::mem::take(&mut records[*next - 1]))
779        }
780        _ => None,
781    };
782    let Some(record) = record else { return Ok(false) };
783    let f = x.sort_file(sd);
784    let (area, size) = f.file.area;
785    let len = record.len().min(size);
786    x.mem()[area..area + len].copy_from_slice(&record[..len]);
787    if f.fixed {
788        x.mem()[area + len..area + size].fill(ebcdic::SPACE);
789    }
790    if let Some(d) = f.file.depending {
791        host::set_integer(x, d.item, len as i64, pos)?;
792    }
793    if let Some(p) = into {
794        let dest = x.locate(p, true)?;
795        let bytes = x.mem()[area..area + if f.fixed { size } else { len }].to_vec();
796        x.assign(dest, Val::Bytes(bytes), None, pos)?;
797    }
798    Ok(true)
799}
800
801/// A table SORT of `count` elements of `stride` bytes from `base`, each moved whole to its place by
802/// its keys, which `keys` gives once the table is known to lie in storage. `name` is the table as
803/// written.
804pub fn sort_table<P: Copy, H: Host<P>>(x: &mut H, base: usize, count: usize, stride: usize, keys: impl FnOnce(&mut H) -> R<Vec<ItemKey>>, name: &str, pos: Pos) -> R<()> {
805    crate::host::unfollowed(x, "SORT and MERGE");
806    if base + count * stride > x.mem().len() {
807        return Err(Abend::ironwork(format!("SORT {name} reaches outside the run unit's storage"), pos));
808    }
809    let keys = keys(x)?;
810    let mut entries = Vec::with_capacity(count);
811    for i in 0..count {
812        let at = base + i * stride;
813        let values = key_values(x, at, &keys, false, pos)?;
814        entries.push(Entry { record: x.mem()[at..at + stride].to_vec(), keys: values });
815    }
816    let up = ascending(&keys);
817    entries.sort_by(|a, b| order(&a.keys, &b.keys, &up));
818    for (i, e) in entries.iter().enumerate() {
819        let at = base + i * stride;
820        x.mem()[at..at + stride].copy_from_slice(&e.record);
821    }
822    Ok(())
823}