Skip to main content

ironwork_rt/
fileio.rs

1//! The file statements (lir.md ยง9.4): OPEN, CLOSE, READ, WRITE, REWRITE, DELETE and START on one
2//! file, FILE STATUS, LINAGE and the print carriage. A sequential file streams through its DD; an
3//! indexed or relative file, or a sequential file opened I-O, is held in memory ([`Keyed`]). Each
4//! verb returns the status its phrases or the file's error path read; the executor runs those.
5
6use crate::abend::Abend;
7use crate::files::{self, Dd, FileStatus, Format, KeySpan, Keyed, Keying, Move, Open, Record};
8use crate::host::{self, Host};
9use crate::linage::{Geometry, Motion, Page};
10use crate::lir::{Access, Carriage, Organization, Spacing, StartRel};
11use crate::printer::{self, Controls};
12use crate::storage::{Loc, Val};
13use crate::store::{self, ProgramFacts};
14use crate::unit::Event;
15use crate::vocab::{Closing, OpenMode, Pos};
16use numeric::VsamOpenFs;
17use numeric::precision::{Fixed, Places};
18use std::cmp::Ordering;
19use zarch::ebcdic;
20
21type R<T> = Result<T, Abend>;
22
23/// A file as SELECT and FD declare it ([`crate::lir::FileDesc`]), with the executor's handles: `P`
24/// a data item, `X` an integer it evaluates. Its keys are the executor's to resolve
25/// ([`Files::keying`]).
26#[derive(Clone, Copy)]
27pub struct File<'a, P, X> {
28    pub index: usize,
29    pub name: &'a str,
30    /// The DD name ASSIGN gives.
31    pub assign: &'a str,
32    pub organization: Organization,
33    pub access: Access,
34    pub optional: bool,
35    /// How records are held when the DD does not say.
36    pub format: Format,
37    pub status: Option<P>,
38    pub relative: Option<P>,
39    pub linage: Option<Linage<X>>,
40    pub carriage: Option<Carriage>,
41    /// Where the record area is in run-unit memory, and its length.
42    pub area: (usize, usize),
43    /// The shortest and longest variable-length record a READ takes without a record length
44    /// conflict ([`crate::lir::FileDesc::read_lengths`]).
45    pub read_lengths: (usize, usize),
46    pub depending: Option<Depending<P>>,
47}
48
49/// RECORD IS VARYING DEPENDING ON: the item holding a record's length, and the shortest and longest
50/// record the clause allows.
51#[derive(Clone, Copy)]
52pub struct Depending<P> {
53    pub item: P,
54    pub lengths: (usize, usize),
55}
56
57/// LINAGE's values, evaluated in this order whenever the page's geometry is taken, and
58/// LINAGE-COUNTER.
59#[derive(Clone, Copy)]
60pub struct Linage<X> {
61    pub lines: X,
62    pub footing: Option<X>,
63    pub top: Option<X>,
64    pub bottom: Option<X>,
65    pub counter: Option<Loc>,
66}
67
68/// READ ([`crate::lir::FileVerb::Read`]). `key` is READ ... KEY IS.
69#[derive(Clone, Copy)]
70pub struct Read<P> {
71    pub sequential: bool,
72    pub previous: bool,
73    pub into: Option<P>,
74    pub key: Option<P>,
75}
76
77/// WRITE's ADVANCING phrase ([`crate::lir::Advance`]). A mnemonic-name's `space` is None when its
78/// environment-name is no printer channel.
79#[derive(Clone, Copy)]
80pub enum Advance<'a, X> {
81    Lines { before: bool, count: X },
82    Page { before: bool },
83    Mnemonic { before: bool, space: Option<Spacing>, name: &'a str, environment: &'a str },
84}
85
86/// How a verb ended.
87pub enum Outcome {
88    /// FILE STATUS holds its status and nothing is left to run.
89    Done,
90    /// The status the statement's AT END (`at_end`) or INVALID KEY phrase, or else the file's
91    /// error path, reads.
92    Status { status: FileStatus, at_end: bool },
93    /// A WRITE to a LINAGE file, with FILE STATUS set: END-OF-PAGE or NOT END-OF-PAGE runs.
94    Page { end_of_page: bool },
95    /// A failing status that takes the file's error path at once.
96    Failed(Failure),
97}
98
99/// `mode` is the mode the file is open in, or being opened in.
100pub struct Failure {
101    pub status: FileStatus,
102    pub mode: Option<OpenMode>,
103    pub message: String,
104}
105
106/// What the file verbs ask of the executor beyond [`Host`].
107pub trait Files<P: Copy, X: Copy>: Host<P> {
108    /// The program's file `k` while it is open.
109    fn slot(&mut self, k: usize) -> &mut Option<Open>;
110    /// Whether CLOSE WITH LOCK has closed file `k`.
111    fn locked(&mut self, k: usize) -> &mut bool;
112    fn dd(&self, assign: &str) -> Option<Dd>;
113    fn notify(&mut self, event: Event<'_>);
114    fn int(&mut self, value: X, pos: Pos) -> R<i64>;
115    /// How file `k` finds its records by key.
116    fn keying(&mut self, k: usize, pos: Pos) -> R<Keying>;
117    /// Which key of indexed file `k` the item names (0 the prime key, then each alternate), and
118    /// its value in the record area. With `partial` (START) it may be a leading part of the key.
119    fn key_value(&mut self, k: usize, keying: &Keying, key: P, partial: bool, pos: Pos) -> R<(usize, Vec<u8>)>;
120}
121
122pub fn set_status<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, status: FileStatus, pos: Pos) -> R<()> {
123    if let Some(p) = file.status {
124        let loc = receiver(x, p)?;
125        let bytes = x.facts().page().encode(status.as_str()).map_err(|e| Abend::ironwork(e.to_string(), pos))?;
126        x.assign(loc, Val::Bytes(bytes), None, pos)?;
127    }
128    Ok(())
129}
130
131/// Locates a receiver the verb only writes, FILE STATUS: its old bytes reach nothing, so they are
132/// not read.
133fn receiver<P: Copy>(x: &mut impl Host<P>, p: P) -> R<Loc> {
134    let was = x.taint().is_some_and(|t| t.writing(true));
135    let loc = x.locate(p, false);
136    if let Some(t) = x.taint() {
137        t.writing(was);
138    }
139    loc
140}
141
142/// A failure in the mode file `k` is open in now.
143fn failed<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, status: FileStatus, message: String) -> Outcome {
144    let mode = x.slot(file.index).as_ref().map(|f| f.mode);
145    Outcome::Failed(Failure { status, mode, message })
146}
147
148pub fn sequential<P, X>(file: &File<'_, P, X>) -> bool {
149    file.access == Access::Sequential || file.organization == Organization::Sequential
150}
151
152/// Whether the file's DD holds a byte before each record for the printer control character: a
153/// print file under ADV, unless the DD is text, which shows the character as line spacing.
154pub fn adds_control_byte<P, X>(file: &File<'_, P, X>, format: Format) -> bool {
155    file.carriage.is_some_and(|c| !c.reserved) && format != Format::Text
156}
157
158/// How the file's DD holds its records.
159pub fn dd_format<P: Copy, X: Copy>(x: &impl Files<P, X>, file: &File<'_, P, X>) -> Format {
160    x.dd(file.assign).and_then(|d| d.format).unwrap_or(file.format)
161}
162
163fn record_area<P: Copy>(x: &mut impl Host<P>, (offset, size): (usize, usize)) -> &[u8] {
164    &x.mem()[offset..offset + size]
165}
166
167/// The prime key's value in the record area, which a random READ, START or DELETE reads.
168fn read_prime_key<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, prime: &KeySpan) -> Vec<u8> {
169    if let Some(taint) = x.taint() {
170        taint.read(file.area.0 + prime.offset, prime.len);
171    }
172    prime.of(record_area(x, file.area))
173}
174
175fn relative_value<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<i64> {
176    let r = file.relative.ok_or_else(|| Abend::ironwork(format!("{} has no RELATIVE KEY", file.name), pos))?;
177    x.integer(r, pos)
178}
179
180/// The RELATIVE KEY's record number as a key, or None when it is below 1 (assumption C331).
181fn relative_number<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<Option<Vec<u8>>> {
182    let n = relative_value(x, file, pos)?;
183    Ok((n >= 1).then(|| files::record_number(n as u64)))
184}
185
186/// Whether record number `n` fits the RELATIVE KEY item, as sequential READ and WRITE store it
187/// there.
188fn relative_fits<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, n: u64) -> R<bool> {
189    let Some(r) = file.relative else { return Ok(true) };
190    let loc = x.locate(r, false)?;
191    Ok(match loc.kind.digits_scale() {
192        Some((digits, scale)) => digits.saturating_sub(scale) >= 19 || n < 10u64.pow(digits.saturating_sub(scale)),
193        None => true,
194    })
195}
196
197/// Runs `op` on file `k` when it is open and held in memory.
198fn held<P: Copy, X: Copy, F: Files<P, X>, T>(x: &mut F, k: usize, op: impl FnOnce(&mut F, OpenMode, Format, &mut Keyed) -> R<T>) -> R<Option<T>> {
199    let Some(mut f) = x.slot(k).take() else { return Ok(None) };
200    let (mode, format) = (f.mode, f.format);
201    let result = match f.keyed() {
202        Some(keyed) => op(x, mode, format, keyed).map(Some),
203        None => Ok(None),
204    };
205    *x.slot(k) = Some(f);
206    result
207}
208
209fn is_held<P: Copy, X: Copy>(x: &mut impl Files<P, X>, k: usize) -> bool {
210    x.slot(k).as_ref().is_some_and(|f| f.is_keyed())
211}
212
213/// Whether the file is a print file opened I-O whose records hold the byte ADV adds
214/// ([`numeric::assumptions::PRINT_FILE_UPDATE`]).
215fn held_control_byte<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>) -> bool {
216    x.slot(file.index).as_ref().is_some_and(|f| f.is_keyed() && adds_control_byte(file, f.format))
217}
218
219/// A record as WRITE or REWRITE puts it in the file: a fixed record at the length of the area.
220fn record_bytes<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, format: Format) -> Vec<u8> {
221    let mut bytes = store::bytes(x.mem(), loc).to_vec();
222    if format != Format::Variable {
223        bytes.resize(file.area.1.max(bytes.len()), ebcdic::SPACE);
224    }
225    bytes
226}
227
228/// The record WRITE, REWRITE or RELEASE puts out: under DEPENDING ON the record area's first n
229/// bytes, n the item's value, or status 44 when n is outside the clause's lengths (Language
230/// Reference SC27-8713-03, pp. 188, 302).
231pub fn record_length<P: Copy, X: Copy>(x: &mut impl Host<P>, file: &File<'_, P, X>, loc: Loc, pos: Pos) -> R<Result<Loc, FileStatus>> {
232    let Some(d) = file.depending else { return Ok(Ok(loc)) };
233    let n = x.integer(d.item, pos)?;
234    let (shortest, longest) = d.lengths;
235    if n < shortest as i64 || n > longest.min(file.area.1) as i64 {
236        return Ok(Err(FileStatus::RecordLengthChanged));
237    }
238    Ok(Ok(Loc { offset: file.area.0, len: n as usize, ..loc }))
239}
240
241/// Moves a record into the file's area, and to INTO's item; a variable-length record fills only
242/// its own length, which DEPENDING ON's item receives. True when the record was longer than the
243/// area.
244fn deliver<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, record: &[u8], variable: bool, into: Option<P>, pos: Pos) -> R<bool> {
245    let (offset, size) = file.area;
246    let n = record.len().min(size);
247    let mem = x.mem();
248    mem[offset..offset + n].copy_from_slice(&record[..n]);
249    if !variable {
250        mem[offset + n..offset + size].fill(ebcdic::SPACE);
251    }
252    if let Some(t) = x.taint() {
253        t.set(offset, n, true);
254        t.set(offset + n, if variable { 0 } else { size - n }, false);
255    }
256    if let Some(d) = file.depending {
257        host::set_integer(x, d.item, n as i64, pos)?;
258    }
259    if let Some(r) = into {
260        let dest = x.locate(r, true)?;
261        let moved = if variable { n } else { size };
262        if let Some(t) = x.taint() {
263            t.read(offset, moved);
264        }
265        let bytes = x.mem()[offset..offset + moved].to_vec();
266        x.assign(dest, Val::Bytes(bytes), None, pos)?;
267    }
268    Ok(record.len() > size)
269}
270
271/// Whether a record READ delivered has a record length conflict, status 04: it was longer than the
272/// record area, or it is a variable-length record outside the file's read lengths
273/// ([`numeric::assumptions::VLR_RECORDS_CHECKED`]).
274fn length_conflict<P, X>(file: &File<'_, P, X>, len: usize, variable: bool, long: bool) -> bool {
275    let (shortest, longest) = file.read_lengths;
276    long || variable && !(shortest..=longest).contains(&len)
277}
278
279/// OPEN: a file whose data set is unavailable is status 35, or 05 when it is OPTIONAL, which
280/// OPEN EXTEND then creates (Language Reference SC27-8713-03, pp. 300-301). A VSAM data set a run
281/// left open for output is verified, status 97 under VSAMOPENFS(COMPAT), which takes the error
282/// path but never ends the run, or 00 under SUCC ([`numeric::assumptions::VSAM_DATA_SET_LEFT_OPEN`]).
283pub fn open<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, mode: OpenMode, pos: Pos) -> R<Outcome> {
284    let (k, name) = (file.index, file.name);
285    let failure = |status, message| Ok(Outcome::Failed(Failure { status, mode: Some(mode), message }));
286    if *x.locked(k) {
287        return failure(FileStatus::ClosedWithLock, format!("{name} was closed WITH LOCK"));
288    }
289    if x.slot(k).is_some() {
290        return failure(FileStatus::AlreadyOpen, format!("{name} is already open"));
291    }
292    let page = match (&file.linage, mode) {
293        (Some(_), OpenMode::Output | OpenMode::Extend) => Some(Page::opened(geometry(x, file, pos)?)),
294        _ => None,
295    };
296    let default = file.format;
297    let dd = x.dd(file.assign);
298    if let Some(d) = &dd {
299        x.notify(Event::Open { dd: file.assign, mode, path: &d.path });
300    }
301    let no_dd = format!("{name}: no DD {} was given (--dd {}=path)", file.assign, file.assign);
302    let held = match file.organization {
303        Organization::Indexed | Organization::Relative => true,
304        Organization::Sequential => mode == OpenMode::InputOutput,
305        Organization::LineSequential => false,
306    };
307    if held {
308        let status = match &dd {
309            Some(d) if mode == OpenMode::Output || d.path.exists() => FileStatus::Success,
310            _ if file.optional && mode != OpenMode::Output => FileStatus::SuccessOptional,
311            None => return failure(FileStatus::FileNotFound, no_dd),
312            Some(d) => return failure(FileStatus::FileNotFound, format!("{name}: {}: no such file", d.path.display())),
313        };
314        let keying = x.keying(k, pos)?;
315        let format = dd.as_ref().and_then(|d| d.format).unwrap_or(default);
316        let record_len = file.area.1 + usize::from(adds_control_byte(file, format));
317        let vsam = dd.as_ref().filter(|_| matches!(file.organization, Organization::Indexed | Organization::Relative));
318        let verified = vsam.is_some_and(|d| files::left_open(&d.path));
319        let mut f = match files::open_keyed(dd.as_ref(), mode, format, keying, record_len, x.facts().page()) {
320            Ok(f) => f,
321            Err(e) => return failure(FileStatus::PermanentError, format!("{name}: {e}")),
322        };
323        if let Some(d) = vsam.filter(|_| mode != OpenMode::Input)
324            && let Err(e) = f.mark_open(&d.path)
325        {
326            return failure(FileStatus::PermanentError, format!("{name}: {}: {e}", files::open_mark(&d.path).display()));
327        }
328        let status = match x.facts().options().vsamopenfs {
329            _ if !verified => status,
330            VsamOpenFs::Compat => FileStatus::SuccessVerified,
331            VsamOpenFs::Succ => FileStatus::Success,
332        };
333        opened(x, file, f, status, pos)?;
334        return Ok(match (status, vsam) {
335            (FileStatus::SuccessVerified, Some(d)) => failed(x, file, status, format!("{name}: {} was left open for output, and OPEN verified it", d.path.display())),
336            _ => Outcome::Done,
337        });
338    }
339    match dd {
340        _ if file.optional && mode == OpenMode::Input && dd.as_ref().is_none_or(|d| !d.path.exists()) => opened(x, file, files::absent(), FileStatus::SuccessOptional, pos),
341        None => failure(FileStatus::FileNotFound, no_dd),
342        Some(dd) if mode == OpenMode::Extend && !file.optional && !dd.path.exists() => failure(FileStatus::FileNotFound, format!("{name}: {}: no such file", dd.path.display())),
343        Some(dd) => {
344            let created = mode == OpenMode::Extend && !dd.path.exists();
345            match files::open(&dd, mode, dd.format.unwrap_or(default)) {
346                Ok(mut f) => {
347                    f.page = page;
348                    opened(x, file, f, if created { FileStatus::SuccessOptional } else { FileStatus::Success }, pos)
349                }
350                Err(e) => {
351                    let status = match e.kind() {
352                        std::io::ErrorKind::NotFound => FileStatus::FileNotFound,
353                        std::io::ErrorKind::Unsupported => FileStatus::OpenModeUnsupported,
354                        _ => FileStatus::PermanentError,
355                    };
356                    failure(status, format!("{name}: {}: {e}", dd.path.display()))
357                }
358            }
359        }
360    }
361}
362
363/// Holds the file open; OPEN sets a LINAGE file's LINAGE-COUNTER to 1 (Language Reference
364/// SC27-8713-03, p. 24).
365fn opened<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, f: Open, status: FileStatus, pos: Pos) -> R<Outcome> {
366    *x.slot(file.index) = Some(f);
367    set_linage_counter(x, file, 1, pos)?;
368    set_status(x, file, status, pos)?;
369    Ok(Outcome::Done)
370}
371
372/// The page the file's LINAGE clause gives as its data items stand now.
373fn geometry<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<Geometry> {
374    let Some(linage) = file.linage else { return Err(Abend::ironwork(format!("{} has no LINAGE clause", file.name), pos)) };
375    let body = x.int(linage.lines, pos)?;
376    let footing = match linage.footing {
377        Some(v) => Some(x.int(v, pos)?),
378        None => None,
379    };
380    let top = match linage.top {
381        Some(v) => x.int(v, pos)?,
382        None => 0,
383    };
384    let bottom = match linage.bottom {
385        Some(v) => x.int(v, pos)?,
386        None => 0,
387    };
388    Geometry::new(body, footing, top, bottom).map_err(|why| Abend::ironwork(format!("{}: {why} ({})", file.name, numeric::assumptions::LINAGE_VALUES), pos))
389}
390
391fn set_linage_counter<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, value: u64, pos: Pos) -> R<()> {
392    let Some(loc) = file.linage.and_then(|l| l.counter) else { return Ok(()) };
393    x.store_fixed(loc, &Fixed::new(value as i128, Places::new(19, 0)), pos)
394}
395
396/// CLOSE, and its phrases as IBM's table has them for a file on a medium without reels or units
397/// (Language Reference for Enterprise COBOL 6.4, 'Effect of CLOSE statement on file types'): REEL
398/// or UNIT leaves the file open and sets status 07, NO REWIND closes it with 07, and LOCK closes it
399/// so that OPEN refuses it with 38.
400pub fn close<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, closing: Option<Closing>, pos: Pos) -> R<Outcome> {
401    let name = file.name;
402    if closing == Some(Closing::Volume) && x.slot(file.index).is_some() {
403        set_status(x, file, FileStatus::SuccessNonReel, pos)?;
404        return Ok(Outcome::Done);
405    }
406    match x.slot(file.index).take() {
407        None => Ok(failed(x, file, FileStatus::NotOpen, format!("{name} is not open"))),
408        Some(f) => {
409            let mode = Some(f.mode);
410            match f.close() {
411                Ok(()) => {
412                    if let Some(d) = x.dd(file.assign) {
413                        x.notify(Event::Close { dd: file.assign, path: &d.path });
414                    }
415                    *x.locked(file.index) |= closing == Some(Closing::Lock);
416                    let status = if closing == Some(Closing::NoRewind) { FileStatus::SuccessNonReel } else { FileStatus::Success };
417                    set_status(x, file, status, pos)?;
418                    Ok(Outcome::Done)
419                }
420                Err(e) => Ok(Outcome::Failed(Failure { status: FileStatus::PermanentError, mode, message: format!("{name}: {e}") })),
421            }
422        }
423    }
424}
425
426/// READ of a held file by key or in sequence, else of a stream, whose phrase is AT END.
427pub fn read<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, r: Read<P>, pos: Pos) -> R<Outcome> {
428    let k = file.index;
429    if !is_held(x, k) {
430        return read_stream(x, file, r.into, pos);
431    }
432    let sequential = r.sequential;
433    let added = held_control_byte(x, file);
434    let (status, found, variable) = held(x, k, |x, mode, format, keyed| {
435        let variable = format == Format::Variable;
436        if !matches!(mode, OpenMode::Input | OpenMode::InputOutput) {
437            return Ok((FileStatus::NotOpenInput, None, variable));
438        }
439        if sequential {
440            return Ok(match keyed.read_next(r.previous) {
441                Err(status) => (status, None, variable),
442                Ok(None) => (FileStatus::AtEnd, None, variable),
443                Ok(Some(found)) if keyed.keying == Keying::Relative && !relative_fits(x, file, files::number_of(&found.key))? => (FileStatus::RelativeKeyOverflow, None, variable),
444                Ok(Some(found)) => (if found.duplicate { FileStatus::SuccessDuplicate } else { FileStatus::Success }, Some(found), variable),
445            });
446        }
447        let keying = keyed.keying.clone();
448        let (which, value) = match (&keying, r.key) {
449            (Keying::Indexed { .. }, Some(key)) => x.key_value(k, &keying, key, false, pos)?,
450            (Keying::Indexed { prime, .. }, None) => (0, read_prime_key(x, file, prime)),
451            _ => match relative_number(x, file, pos)? {
452                Some(key) => (0, key),
453                None => {
454                    keyed.lose_position();
455                    return Ok((FileStatus::NotFound, None, variable));
456                }
457            },
458        };
459        Ok(match keyed.get(which, &value) {
460            None => {
461                keyed.lose_position();
462                (FileStatus::NotFound, None, variable)
463            }
464            Some(found) => {
465                keyed.read_at(which, &found.key);
466                (if found.duplicate { FileStatus::SuccessDuplicate } else { FileStatus::Success }, Some(found), variable)
467            }
468        })
469    })?
470    .unwrap_or((FileStatus::NotOpenInput, None, false));
471    let mut status = status;
472    if let Some(found) = found {
473        if sequential && file.organization == Organization::Relative
474            && let Some(rk) = file.relative
475        {
476            host::set_integer(x, rk, files::number_of(&found.key) as i64, pos)?;
477        }
478        let record = found.record.get(usize::from(added)..).unwrap_or_default();
479        let long = deliver(x, file, record, variable, r.into, pos)?;
480        if length_conflict(file, record.len(), variable, long) && status == FileStatus::Success {
481            status = FileStatus::SuccessWrongLength;
482        }
483    }
484    Ok(Outcome::Status { status, at_end: sequential })
485}
486
487fn read_stream<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, into: Option<P>, pos: Pos) -> R<Outcome> {
488    let k = file.index;
489    let at_end = |status| Ok(Outcome::Status { status, at_end: true });
490    let size = file.area.1;
491    let Some(mut f) = x.slot(k).take() else {
492        return at_end(FileStatus::NotOpenInput);
493    };
494    let added = adds_control_byte(file, f.format);
495    let read = f.read(size + usize::from(added));
496    let format = f.format;
497    let input = f.mode == OpenMode::Input;
498    *x.slot(k) = Some(f);
499    if !input {
500        return at_end(FileStatus::NotOpenInput);
501    }
502    let (record, wrong_length) = match read {
503        Err(e) => return Ok(failed(x, file, FileStatus::PermanentError, format!("READ {}: {e}", file.name))),
504        Ok(Record::End) => return at_end(FileStatus::AtEnd),
505        Ok(Record::Data(bytes)) => (bytes, false),
506        Ok(Record::WrongLength(bytes)) => (bytes, true),
507    };
508    let record = if added { record.get(1..).unwrap_or_default().to_vec() } else { record };
509    let record = if format == Format::Text {
510        let page = x.facts().page();
511        let unknown = page.encode_char('?').unwrap_or(0x6F);
512        String::from_utf8_lossy(&record).chars().map(|c| page.encode_char(c).unwrap_or(unknown)).collect()
513    } else {
514        record
515    };
516    let variable = format == Format::Variable;
517    let long = deliver(x, file, &record, variable, into, pos)?;
518    at_end(if wrong_length || length_conflict(file, record.len(), variable, long) { FileStatus::SuccessWrongLength } else { FileStatus::Success })
519}
520
521/// WRITE of the record at `loc`: to a LINAGE file's page, a stream, or a held file.
522pub fn write<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, advancing: Option<Advance<'_, X>>, pos: Pos) -> R<Outcome> {
523    let k = file.index;
524    let loc = match record_length(x, file, loc, pos)? {
525        Ok(loc) => loc,
526        Err(status) => return Ok(Outcome::Status { status, at_end: false }),
527    };
528    if paged(x, k) {
529        return write_page(x, file, loc, advancing, pos);
530    }
531    if !is_held(x, k) {
532        let (before, space) = match advancing {
533            Some(a) => advance(x, a, pos)?,
534            None => (false, Spacing::Lines(1)),
535        };
536        return write_stream(x, file, loc, before, space, pos);
537    }
538    let sequential = sequential(file);
539    let status = held(x, k, |x, mode, format, keyed| {
540        let allowed = match mode {
541            OpenMode::Output => true,
542            OpenMode::Extend => sequential,
543            OpenMode::InputOutput => !sequential,
544            OpenMode::Input => false,
545        };
546        if !allowed || keyed.keying == Keying::Position {
547            return Ok(FileStatus::NotOpenOutput);
548        }
549        keyed.last_read = None;
550        let bytes = record_bytes(x, file, loc, format);
551        let key = match &keyed.keying {
552            Keying::Indexed { prime, .. } => {
553                let key = prime.of(&bytes);
554                if sequential && keyed.highest_key().is_some_and(|h| key <= *h) {
555                    return Ok(FileStatus::SequenceError);
556                }
557                key
558            }
559            _ if sequential => {
560                let n = keyed.highest_key().map_or(0, |h| files::number_of(h)) + 1;
561                if n > files::MAX_RELATIVE || !relative_fits(x, file, n)? {
562                    return Ok(FileStatus::BoundaryViolation);
563                }
564                if let Some(rk) = file.relative {
565                    host::set_integer(x, rk, n as i64, pos)?;
566                }
567                files::record_number(n)
568            }
569            _ => match relative_number(x, file, pos)? {
570                Some(key) if files::number_of(&key) <= files::MAX_RELATIVE => key,
571                _ => return Ok(FileStatus::BoundaryViolation),
572            },
573        };
574        Ok(match keyed.insert(key, bytes) {
575            Err(status) => status,
576            Ok(true) => FileStatus::SuccessDuplicate,
577            Ok(false) => FileStatus::Success,
578        })
579    })?
580    .unwrap_or(FileStatus::NotOpenOutput);
581    Ok(Outcome::Status { status, at_end: false })
582}
583
584fn paged<P: Copy, X: Copy>(x: &mut impl Files<P, X>, k: usize) -> bool {
585    x.slot(k).as_ref().is_some_and(|f| f.page.is_some())
586}
587
588/// A WRITE to a LINAGE file: the page decides how far the paper moves, and once the record is
589/// written LINAGE-COUNTER changes and an END-OF-PAGE phrase runs (Language Reference
590/// SC27-8713-03, pp. 474-475; Programming Guide SC27-8714-03, p. 178).
591fn write_page<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, advancing: Option<Advance<'_, X>>, pos: Pos) -> R<Outcome> {
592    let k = file.index;
593    let (before, motion) = match advancing {
594        None => (false, Motion::Lines(1)),
595        Some(Advance::Lines { before, count }) => (before, Motion::Lines(x.int(count, pos)?.max(0) as u64)),
596        Some(Advance::Page { before }) => (before, Motion::Page),
597        Some(Advance::Mnemonic { name, .. }) => {
598            return Err(Abend::ironwork(format!("ADVANCING {name} on {}, whose FD has LINAGE, is not supported yet", file.name), pos));
599        }
600    };
601    let Some(mut page) = x.slot(k).as_ref().and_then(|f| f.page) else { return Ok(Outcome::Done) };
602    let step = page.write(before, motion, || geometry(x, file, pos))?;
603    let controls = file.carriage.map(|c| printer::moving(c.machine, step.ahead, step.behind));
604    let text = (Some(Move::Lines(step.ahead)), before.then_some(Move::Lines(step.behind)));
605    if let failure @ Outcome::Failed(_) = put_line(x, file, loc, controls, text, pos)? {
606        return Ok(failure);
607    }
608    if let Some(f) = x.slot(k).as_mut() {
609        f.page = Some(page);
610    }
611    set_linage_counter(x, file, page.counter, pos)?;
612    Ok(Outcome::Page { end_of_page: step.end_of_page })
613}
614
615/// A WRITE's ADVANCING phrase as a movement, BEFORE or AFTER the line; a count below zero moves as
616/// zero ([`numeric::assumptions::PRINT_CONTROL_RUN_TIME`]).
617fn advance<P: Copy, X: Copy>(x: &mut impl Files<P, X>, a: Advance<'_, X>, pos: Pos) -> R<(bool, Spacing)> {
618    Ok(match a {
619        Advance::Lines { before, count } => (before, Spacing::Lines(x.int(count, pos)?.max(0) as u64)),
620        Advance::Page { before } => (before, Spacing::Channel(1)),
621        Advance::Mnemonic { before, space, name, environment } => {
622            let space = space.ok_or_else(|| Abend::ironwork(format!("ADVANCING {name}: {environment} is not a printer channel"), pos))?;
623            (before, space)
624        }
625    })
626}
627
628/// A WRITE to a sequential file: a print file's records carry the control character, a text DD
629/// shows it as line and form feeds.
630pub fn write_stream<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, before: bool, space: Spacing, pos: Pos) -> R<Outcome> {
631    let controls = file.carriage.map(|c| printer::controls(c.machine, before, space));
632    put_line(x, file, loc, controls, printer::text_motion(before, space), pos)
633}
634
635/// Writes the record at `loc` to the sequential file behind `controls`, or to a text DD with the
636/// paper moved as `text` says before and after its line.
637fn put_line<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, controls: Option<Controls>, text: (Option<Move>, Option<Move>), pos: Pos) -> R<Outcome> {
638    let (k, name) = (file.index, file.name);
639    let Some(mut f) = x.slot(k).take() else {
640        return Ok(failed(x, file, FileStatus::NotOpenOutput, format!("WRITE {name}: {}", FileStatus::NotOpenOutput.meaning())));
641    };
642    if f.mode == OpenMode::Input {
643        *x.slot(k) = Some(f);
644        return Ok(failed(x, file, FileStatus::NotOpenOutput, format!("WRITE {name}: {}", FileStatus::NotOpenOutput.meaning())));
645    }
646    let reserved = usize::from(file.carriage.is_some_and(|c| c.reserved));
647    if let Some(c) = controls.filter(|_| reserved == 1 && loc.len > 0) {
648        x.mem()[loc.offset] = c.data;
649        host::mark(x, loc.offset, 1);
650    }
651    let bytes = record_bytes(x, file, loc, f.format);
652    let written = match (f.format, controls) {
653        (Format::Text, _) => {
654            let line = x.facts().page().decode(bytes.get(reserved..).unwrap_or_default()).trim_end().to_owned();
655            f.print(text.0, &line, text.1)
656        }
657        (_, None) => f.write(&bytes),
658        (_, Some(c)) => c.records().try_for_each(|(control, line)| {
659            let mut record = Vec::with_capacity(bytes.len() + 1);
660            record.push(control);
661            if line {
662                record.extend_from_slice(&bytes[reserved.min(bytes.len())..]);
663            } else {
664                record.resize(bytes.len() + 1 - reserved.min(bytes.len()), ebcdic::SPACE);
665            }
666            f.write(&record)
667        }),
668    };
669    *x.slot(k) = Some(f);
670    match written {
671        Ok(()) => set_status(x, file, FileStatus::Success, pos).map(|()| Outcome::Done),
672        Err(e) => Ok(failed(x, file, FileStatus::PermanentError, format!("WRITE {name}: {e}"))),
673    }
674}
675
676/// REWRITE of the record at `loc`; its phrase is INVALID KEY.
677pub fn rewrite<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, pos: Pos) -> R<FileStatus> {
678    let loc = match record_length(x, file, loc, pos)? {
679        Ok(loc) => loc,
680        Err(status) => return Ok(status),
681    };
682    let sequential = sequential(file);
683    let added = held_control_byte(x, file);
684    Ok(held(x, file.index, |x, mode, format, keyed| {
685        if mode != OpenMode::InputOutput {
686            return Ok(FileStatus::NotOpenInputOutput);
687        }
688        let mut bytes = record_bytes(x, file, loc, format);
689        if added {
690            let control = keyed.last_read.as_ref().and_then(|key| keyed.record(key)).and_then(|old| old.first().copied()).unwrap_or(ebcdic::SPACE);
691            bytes.insert(0, control);
692        }
693        let prior = keyed.last_read.take();
694        let key = if sequential {
695            let Some(prior) = prior else { return Ok(FileStatus::NoPriorRead) };
696            if keyed.prime_key(&bytes).is_some_and(|key| key != prior) {
697                return Ok(FileStatus::SequenceError);
698            }
699            prior
700        } else {
701            match keyed.prime_key(&bytes) {
702                Some(key) => key,
703                None => match relative_number(x, file, pos)? {
704                    Some(key) => key,
705                    None => return Ok(FileStatus::NotFound),
706                },
707            }
708        };
709        if keyed.keying == Keying::Position && keyed.record(&key).is_some_and(|old| old.len() != bytes.len()) {
710            return Ok(FileStatus::RecordLengthChanged);
711        }
712        Ok(match keyed.replace(key, bytes) {
713            Err(status) => status,
714            Ok(true) => FileStatus::SuccessDuplicate,
715            Ok(false) => FileStatus::Success,
716        })
717    })?
718    .unwrap_or(FileStatus::NotOpenInputOutput))
719}
720
721/// DELETE; its phrase is INVALID KEY.
722pub fn delete<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<FileStatus> {
723    let sequential = sequential(file);
724    Ok(held(x, file.index, |x, mode, _, keyed| {
725        if mode != OpenMode::InputOutput || keyed.keying == Keying::Position {
726            return Ok(FileStatus::NotOpenInputOutput);
727        }
728        let prior = keyed.last_read.take();
729        let key = match (&keyed.keying, sequential) {
730            (_, true) => match prior {
731                Some(key) => key,
732                None => return Ok(FileStatus::NoPriorRead),
733            },
734            (Keying::Indexed { prime, .. }, false) => read_prime_key(x, file, prime),
735            _ => match relative_number(x, file, pos)? {
736                Some(key) => key,
737                None => return Ok(FileStatus::NotFound),
738            },
739        };
740        Ok(if keyed.remove(&key).is_some() { FileStatus::Success } else { FileStatus::NotFound })
741    })?
742    .unwrap_or(FileStatus::NotOpenInputOutput))
743}
744
745/// START; `key` is START ... KEY, and its phrase is INVALID KEY.
746pub fn start<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, rel: StartRel, key: Option<P>, pos: Pos) -> R<FileStatus> {
747    let (wanted, or_equal) = match rel {
748        StartRel::Equal => (Ordering::Equal, false),
749        StartRel::Greater => (Ordering::Greater, false),
750        StartRel::NotLess => (Ordering::Greater, true),
751    };
752    let k = file.index;
753    Ok(held(x, k, |x, mode, _, keyed| {
754        if !matches!(mode, OpenMode::Input | OpenMode::InputOutput) || keyed.keying == Keying::Position {
755            return Ok(FileStatus::NotOpenInput);
756        }
757        let keying = keyed.keying.clone();
758        let (which, value) = match (&keying, key) {
759            (Keying::Indexed { .. }, Some(r)) => x.key_value(k, &keying, r, true, pos)?,
760            (Keying::Indexed { prime, .. }, None) => (0, read_prime_key(x, file, prime)),
761            (_, Some(r)) => (0, files::record_number(x.integer(r, pos)?.max(0) as u64)),
762            (_, None) => (0, files::record_number(relative_value(x, file, pos)?.max(0) as u64)),
763        };
764        Ok(if keyed.start(which, wanted, or_equal, &value) { FileStatus::Success } else { FileStatus::NotFound })
765    })?
766    .unwrap_or(FileStatus::NotOpenInput))
767}