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