Skip to main content

ironwork_rt/
store.rs

1//! Storage and MOVE: an item's bytes and value by its `Loc`, numeric stores with their size-error
2//! and TRUNC(OPT) rules, MOVE, and the comparisons and class tests of conditions.
3
4use crate::abend::{Abend, AbendCode};
5use crate::codec;
6use crate::edit;
7use crate::fixed::{MAX_DIGITS, align, compare_fixed, fixed, places_of, pow10, scaled_down, scaled_up, zoned_digits};
8use crate::lir::{ByteClass, SignTest};
9use crate::picture::Sym;
10use crate::storage::{Kind, Loc, Val};
11use crate::unit::{Loader, RunUnit};
12use crate::vocab::{Figurative, Pos, SignClause, SignPosition};
13use numeric::binary::{self, Binary};
14use numeric::precision::{Fixed, Places};
15use numeric::{Numproc, Options, Quote, Trunc, float, sign};
16use std::cmp::Ordering;
17use zarch::check::{ProgramCheck, ProgramMask};
18use zarch::decimal::{self, Decimal};
19use zarch::ebcdic::{self, CodePage, Collation};
20use zarch::hfp::{Hfp, Precision};
21use zarch::wide::U256;
22
23type R<T> = Result<T, Abend>;
24
25/// What these semantics read of the running program beyond a `Loc`: its options, code page and
26/// collating sequence, its edited PICTUREs, and its items by the index a `Loc` carries.
27pub trait ProgramFacts {
28    fn options(&self) -> Options;
29    fn page(&self) -> &'static CodePage;
30    /// A figurative constant's character: HIGH-VALUE and LOW-VALUE are the collating sequence's.
31    fn figurative(&self, f: Figurative) -> u8;
32    fn collation(&self) -> &Collation;
33    /// A character's ordinal position in the collating sequence, from 1, which FUNCTION ORD gives.
34    fn ordinal(&self, byte: u8) -> u16;
35    /// The character at an ordinal position, which FUNCTION CHAR gives.
36    fn character(&self, ordinal: i64) -> Option<u8>;
37    /// How many characters the collating sequence orders.
38    fn characters(&self) -> usize;
39    fn decimal_point(&self) -> char;
40    /// An edited PICTURE's symbols, and the currency sign it shows.
41    fn edit(&self, edit: u32) -> (&[Sym], &str);
42    /// PICTURE P positions to the right of an item's digits.
43    fn scaling(&self, item: usize) -> u32;
44    /// The item a TRUNC(OPT) report names.
45    fn item_name(&self, item: usize) -> String;
46    /// What NUMCHECK(ZON(LAX)) tolerates in a zoned item because of the item it redefines.
47    fn lax_redefinition(&self, _item: usize) -> Option<LaxRedefinition> {
48        None
49    }
50    /// Whether the compiler removed NUMCHECK's test of `item` where the reference at `pos` reads
51    /// it, having found the test always fails.
52    fn numcheck_removed(&self, _item: usize, _pos: Pos) -> bool {
53        false
54    }
55}
56
57/// The two redefinitions NUMCHECK(ZON(LAX)) tolerates (Programming Guide SC27-8714-03, pp. 390-391).
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum LaxRedefinition {
60    /// An unsigned item whose last byte is the last of a signed trailing-overpunch level-01 or
61    /// level-77 item it redefines: tested as signed.
62    Signed,
63    /// A zoned item starting where a level-01 or level-77 numeric-edited item it redefines starts:
64    /// this many of its leading bytes may hold spaces, those over the edited item's leading Z
65    /// positions.
66    LeadingSpaces(u32),
67}
68
69pub fn bytes(mem: &[u8], loc: Loc) -> &[u8] {
70    &mem[loc.offset..loc.offset + loc.len]
71}
72
73pub fn write(mem: &mut [u8], loc: Loc, bytes: &[u8]) {
74    mem[loc.offset..loc.offset + loc.len].copy_from_slice(bytes);
75}
76
77/// PICTURE scaling positions to the right of a numeric item's digits.
78pub fn scaling(facts: &dyn ProgramFacts, loc: Loc) -> u32 {
79    match loc.kind {
80        Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::NumericEdited { .. } => facts.scaling(loc.item),
81        _ => 0,
82    }
83}
84
85/// The places of the value a numeric item holds, its scaling positions included.
86pub fn places(facts: &dyn ProgramFacts, loc: Loc) -> Places {
87    let places = places_of(loc.kind);
88    Places::new(places.int + scaling(facts, loc), places.dec)
89}
90
91pub fn read(facts: &dyn ProgramFacts, mem: &[u8], loc: Loc, pos: Pos) -> R<Val> {
92    let value = read_stored(facts, mem, loc, pos)?;
93    Ok(match value {
94        Val::Num(f) => Val::Num(scaled_up(f, scaling(facts, loc))),
95        other => other,
96    })
97}
98
99/// An item's value as its digits hold it, before any scaling positions to their right.
100pub fn read_stored(facts: &dyn ProgramFacts, mem: &[u8], loc: Loc, pos: Pos) -> R<Val> {
101    let cleaned = facts.options().invdata.is_some_and(|i| i.cleansign).then(|| sign_cleaned(bytes(mem, loc), loc.kind)).flatten();
102    let bytes = cleaned.as_deref().unwrap_or(bytes(mem, loc));
103    let places = places_of(loc.kind);
104    Ok(match loc.kind {
105        Kind::Group | Kind::Alnum { .. } | Kind::NumericEdited { .. } | Kind::AlnumEdited { .. } => Val::Bytes(bytes.to_vec()),
106        Kind::National => Val::National(bytes.to_vec()),
107        Kind::Pointer | Kind::ObjectReference | Kind::ProgramPointer => Val::Address(u32::from_be_bytes(bytes.try_into().unwrap())),
108        Kind::Index => Val::Num(Fixed::new(i32::from_be_bytes(bytes.try_into().unwrap()) as i128, Places::new(9, 0))),
109        Kind::Float(p) => Val::Float(Hfp::from_bytes(p, bytes)),
110        Kind::Binary { digits, signed, native, .. } => Val::Num(Fixed::new(Binary { digits: digits as u8, signed, native }.load(bytes), places)),
111        Kind::Packed { signed, .. } => {
112            let d = codec::packed(bytes, signed, facts.options().numproc).map_err(|c| Abend::check(c, pos))?;
113            Val::Num(fixed(d.negative, U256::from_u128(d.magnitude), places))
114        }
115        Kind::Zoned { signed, sign, .. } => Val::Num(zoned_value(facts.options().numproc, bytes, signed, sign, places, pos)?),
116    })
117}
118
119/// INVDATA(CLEANSIGN): a zoned or packed item whose sign half-byte is not a sign code (0 to 9) is
120/// read with that half-byte made F, positive (assumption C222); None when there is nothing to
121/// clean. A separate sign is a character, not a half-byte, and is left alone.
122fn sign_cleaned(bytes: &[u8], kind: Kind) -> Option<Vec<u8>> {
123    let (at, high) = match kind {
124        Kind::Packed { .. } => (bytes.len().checked_sub(1)?, false),
125        Kind::Zoned { sign: Some(SignClause { separate: true, .. }), .. } => return None,
126        Kind::Zoned { sign: Some(SignClause { position: SignPosition::Leading, .. }), .. } => (0, true),
127        Kind::Zoned { .. } => (bytes.len().checked_sub(1)?, true),
128        _ => return None,
129    };
130    let half = if high { bytes[at] >> 4 } else { bytes[at] & 0x0F };
131    if half > 9 {
132        return None;
133    }
134    let mut cleaned = bytes.to_vec();
135    cleaned[at] |= if high { 0xF0 } else { 0x0F };
136    Some(cleaned)
137}
138
139/// A zoned operand enters arithmetic through PACK, which keeps only the sign's zone.
140pub fn zoned_value(numproc: Numproc, bytes: &[u8], signed: bool, sign: Option<SignClause>, places: Places, pos: Pos) -> R<Fixed> {
141    let d = codec::zoned(bytes, signed, sign, numproc).map_err(|c| Abend::check(c, pos))?;
142    Ok(fixed(d.negative, U256::from_u128(d.magnitude), places))
143}
144
145/// A value as STRING, UNSTRING and INSPECT see an operand that is not an item: its bytes, a
146/// figurative constant as one character, a numeric literal as its digits.
147pub fn natural_bytes(facts: &dyn ProgramFacts, val: Val, pos: Pos) -> R<Vec<u8>> {
148    Ok(match val {
149        Val::Bytes(b) | Val::All(b) | Val::National(b) => b,
150        Val::Fig(f) => vec![facts.figurative(f)],
151        Val::Num(f) => zoned_digits(f.magnitude.to_u128().unwrap_or(0), f.places.total() as usize, decimal::UNSIGNED),
152        _ => return Err(Abend::ironwork("this operand has no characters to work on", pos)),
153    })
154}
155
156pub fn set_integer<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, dest: Loc, value: i64, pos: Pos) -> R<()> {
157    store_fixed(facts, unit, dest, &Fixed::new(value as i128, Places::new(19, 0)), false, pos)
158}
159
160/// Stores an arithmetic result; returns whether it was a size error.
161pub fn store_value<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, loc: Loc, value: Val, rounded: bool, keep_on_size_error: bool, pos: Pos) -> R<bool> {
162    match (loc.kind, value) {
163        (Kind::Float(p), Val::Float(h)) => {
164            let h = if h.precision.digits() > p.digits() { float::narrow_rounded(h, p).map_err(|c| Abend::check(c, pos))? } else { h.lengthen(p) };
165            write(&mut unit.mem, loc, &h.to_bytes());
166            Ok(false)
167        }
168        (Kind::Float(p), Val::Num(f)) => {
169            let h = float::from_fixed(f, p, ProgramMask::default()).map_err(|c| Abend::check(c, pos))?;
170            write(&mut unit.mem, loc, &h.to_bytes());
171            Ok(false)
172        }
173        (_, Val::Float(h)) => {
174            let (f, overflow) = float::to_receiver(h, places(facts, loc));
175            if overflow && keep_on_size_error {
176                return Ok(true);
177            }
178            Ok(store_fixed_checked(facts, unit, loc, &f, false, keep_on_size_error, pos)? || overflow)
179        }
180        (_, Val::Num(f)) => store_fixed_checked(facts, unit, loc, &f, rounded, keep_on_size_error, pos),
181        _ => Err(Abend::ironwork("a non-numeric arithmetic result", pos)),
182    }
183}
184
185pub fn store_fixed<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, loc: Loc, value: &Fixed, rounded: bool, pos: Pos) -> R<()> {
186    store_fixed_checked(facts, unit, loc, value, rounded, false, pos).map(|_| ())
187}
188
189/// Stores a value into a numeric item; returns whether it was a size error, and with
190/// `keep_on_size_error` leaves the item unchanged on one.
191pub fn store_fixed_checked<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, loc: Loc, value: &Fixed, rounded: bool, keep_on_size_error: bool, pos: Pos) -> R<bool> {
192    let beyond = || Abend::ironwork("a value wider than 256 bits", pos);
193    let value = &scaled_down(*value, scaling(facts, loc));
194    let options = facts.options();
195    let (bytes, size_error) = match loc.kind {
196        Kind::Zoned { digits, scale, signed, sign } => {
197            let m = align(value, scale, rounded).ok_or_else(beyond)?;
198            let cap = pow10(digits);
199            let kept = m.div_rem(cap).1.to_u128().unwrap();
200            let negative = signed && value.negative && kept != 0;
201            (zoned_image(kept, digits, signed, negative, sign), m >= cap)
202        }
203        Kind::Packed { digits, scale, signed } => {
204            let m = align(value, scale, rounded).ok_or_else(beyond)?;
205            let cap = pow10(digits);
206            let kept = m.div_rem(cap).1.to_u128().unwrap();
207            let mut out = vec![0u8; loc.len];
208            decimal::encode(&mut out, Decimal { negative: signed && value.negative && kept != 0, magnitude: kept }).map_err(|c| Abend::check(c, pos))?;
209            if !signed {
210                *out.last_mut().unwrap() |= 0x0F;
211            }
212            (out, m >= cap)
213        }
214        Kind::Binary { digits, scale, signed, native } => {
215            let m = align(value, scale, rounded).ok_or_else(beyond)?;
216            let magnitude = m.to_u128().and_then(|m| i128::try_from(m).ok()).ok_or_else(beyond)?;
217            let v = if value.negative { -magnitude } else { magnitude };
218            let item = Binary { digits: digits as u8, signed, native };
219            let stored = binary::store(item, v, &options);
220            if let Some(d) = stored.divergence {
221                let name = facts.item_name(loc.item);
222                let _ = writeln!(
223                    unit.err,
224                    "ironwork: {pos}: TRUNC(OPT) store of {} into {name} PIC {}9({digits}) BINARY: the PICTURE keeps {}, the binary field {}; {} was stored (-silent stops these reports)",
225                    d.value,
226                    if signed { "S" } else { "" },
227                    d.decimal,
228                    d.binary,
229                    d.binary
230                );
231            }
232            let bits = 8 * item.bytes() as u32;
233            let binary_range = if signed { v >= -(1i128 << (bits - 1)) && v < (1i128 << (bits - 1)) } else { (0..(1i128 << bits)).contains(&v.abs()) };
234            let exceeds = if native || options.trunc == Trunc::Bin { !binary_range } else { v.unsigned_abs() >= 10u128.pow(digits) };
235            (stored.bytes, exceeds)
236        }
237        Kind::Float(p) => (float::from_fixed(*value, p, ProgramMask::default()).map_err(|c| Abend::check(c, pos))?.to_bytes(), false),
238        Kind::Index => {
239            let whole = align(value, 0, false).and_then(|m| m.to_u128()).and_then(|m| i32::try_from(m).ok()).ok_or_else(beyond)?;
240            ((if value.negative { -whole } else { whole }).to_be_bytes().to_vec(), false)
241        }
242        Kind::NumericEdited { edit, digits, scale, blank_when_zero } => {
243            let m = align(value, scale, rounded).ok_or_else(beyond)?;
244            let cap = pow10(digits);
245            let kept = m.div_rem(cap).1.to_u128().unwrap();
246            let (syms, currency) = facts.edit(edit);
247            let text = edit::numeric(syms, digits, value.negative && kept != 0, kept, blank_when_zero, facts.decimal_point(), currency);
248            (facts.page().encode(&text).map_err(|e| Abend::ironwork(e.to_string(), pos))?, m >= cap)
249        }
250        _ => return Err(Abend::ironwork("a numeric value stored into a non-numeric item", pos)),
251    };
252    if size_error && keep_on_size_error {
253        return Ok(true);
254    }
255    write(&mut unit.mem, loc, &bytes);
256    Ok(size_error)
257}
258
259/// A zoned item's bytes for a magnitude, with its sign as the SIGN clause places it.
260pub fn zoned_image(magnitude: u128, digits: u32, signed: bool, negative: bool, sign: Option<SignClause>) -> Vec<u8> {
261    let zone = match (signed, negative) {
262        (false, _) => decimal::UNSIGNED,
263        (true, true) => decimal::MINUS,
264        (true, false) => decimal::PLUS,
265    };
266    match sign {
267        Some(SignClause { separate: true, position }) => {
268            let body = zoned_digits(magnitude, digits as usize, decimal::UNSIGNED);
269            let s = if negative { 0x60 } else { 0x4E };
270            if position == SignPosition::Leading { [&[s][..], &body].concat() } else { [&body[..], &[s]].concat() }
271        }
272        Some(SignClause { separate: false, position: SignPosition::Leading }) => {
273            let mut body = zoned_digits(magnitude, digits as usize, decimal::UNSIGNED);
274            body[0] = (zone << 4) | (body[0] & 0x0F);
275            body
276        }
277        _ => zoned_digits(magnitude, digits as usize, zone),
278    }
279}
280
281pub fn figurative_unit(f: Figurative, quote: Quote) -> u16 {
282    match f {
283        Figurative::Zero => 0x0030,
284        Figurative::Space => 0x0020,
285        Figurative::HighValue => 0xFFFF,
286        Figurative::LowValue => 0x0000,
287        Figurative::Quote => quote.unit(),
288        Figurative::Null => 0,
289    }
290}
291
292/// MOVE, and VALUE at start-up, into one receiving item. `src` is the sending item, when there is
293/// one.
294pub fn assign<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, dest: Loc, val: Val, src: Option<Loc>, pos: Pos) -> R<()> {
295    if let Some(s) = src
296        && s.kind == Kind::Group
297        && matches!(dest.kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_) | Kind::NumericEdited { .. } | Kind::AlnumEdited { .. })
298    {
299        // A group move converts nothing (Language Reference SC27-8713-03, p. 410).
300        let mut out = vec![ebcdic::SPACE; dest.len];
301        let n = s.len.min(dest.len);
302        out[..n].copy_from_slice(&bytes(&unit.mem, s)[..n]);
303        write(&mut unit.mem, dest, &out);
304        return Ok(());
305    }
306    let page = facts.page();
307    match dest.kind {
308        Kind::Group | Kind::Alnum { .. } => {
309            let justified = matches!(dest.kind, Kind::Alnum { justified: true });
310            let image = match (dest.kind, src) {
311                (Kind::Group, Some(s)) if matches!(val, Val::Num(_) | Val::Float(_) | Val::Address(_)) || is_decimal(s.kind) => bytes(&unit.mem, s).to_vec(),
312                _ => alnum_image(facts, &val, src, dest.len, pos)?,
313            };
314            let mut out = vec![ebcdic::SPACE; dest.len];
315            if justified && image.len() < dest.len {
316                out[dest.len - image.len()..].copy_from_slice(&image);
317            } else if justified {
318                out.copy_from_slice(&image[image.len() - dest.len..]);
319            } else {
320                let n = image.len().min(dest.len);
321                out[..n].copy_from_slice(&image[..n]);
322            }
323            write(&mut unit.mem, dest, &out);
324        }
325        Kind::National => {
326            let units: Vec<u16> = match val {
327                Val::National(b) => b.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect(),
328                Val::Bytes(b) => page.decode(&b).encode_utf16().collect(),
329                Val::Fig(f) => vec![figurative_unit(f, facts.options().quote); dest.len / 2],
330                _ => return Err(Abend::ironwork("this value cannot be moved to a national item", pos)),
331            };
332            let mut out: Vec<u8> = units.iter().take(dest.len / 2).flat_map(|u| u.to_be_bytes()).collect();
333            while out.len() < dest.len {
334                out.extend_from_slice(&0x0020u16.to_be_bytes());
335            }
336            write(&mut unit.mem, dest, &out);
337        }
338        Kind::Pointer | Kind::ObjectReference | Kind::ProgramPointer => match val {
339            Val::Address(a) => write(&mut unit.mem, dest, &a.to_be_bytes()),
340            Val::Fig(Figurative::Null) => write(&mut unit.mem, dest, &[0; 4]),
341            _ => return Err(Abend::ironwork("a pointer takes an address: use SET ... TO ADDRESS OF or NULL", pos)),
342        },
343        Kind::Index => match val {
344            Val::Num(f) => store_fixed(facts, unit, dest, &f, false, pos)?,
345            _ => return Err(Abend::ironwork("an index takes an occurrence number", pos)),
346        },
347        Kind::AlnumEdited { edit } => {
348            let (syms, _) = facts.edit(edit);
349            let positions = syms.iter().filter(|s| !matches!(s, Sym::Insert(_))).count();
350            let image = alnum_image(facts, &val, src, positions, pos)?;
351            let out = edit::alphanumeric(syms, &image, ebcdic::SPACE, |c| page.encode_char(c).unwrap_or(ebcdic::SPACE));
352            write(&mut unit.mem, dest, &out);
353        }
354        Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::NumericEdited { .. } => match val {
355            Val::Num(f) => {
356                if let Some(s) = src
357                    && packed_copy(facts, s, dest)
358                {
359                    let copied = sign::move_packed(bytes(&unit.mem, s), true, Numproc::Pfd);
360                    write(&mut unit.mem, dest, &copied);
361                } else {
362                    store_fixed(facts, unit, dest, &f, false, pos)?;
363                }
364            }
365            Val::Float(h) => {
366                let (f, _) = float::to_receiver(h, places(facts, dest));
367                store_fixed(facts, unit, dest, &f, false, pos)?;
368            }
369            Val::Fig(Figurative::Zero) => store_fixed(facts, unit, dest, &Fixed::new(0, Places::new(1, 0)), false, pos)?,
370            Val::Fig(f) => write(&mut unit.mem, dest, &vec![facts.figurative(f); dest.len]),
371            Val::All(b) => {
372                let fill: Vec<u8> = b.iter().copied().cycle().take(dest.len).collect();
373                write(&mut unit.mem, dest, &fill);
374            }
375            Val::Bytes(b) if let Some(s) = src
376                && is_decimal(s.kind)
377                && is_decimal(dest.kind) =>
378            {
379                carry_digits(facts, unit, dest, s, &b, pos)?;
380            }
381            Val::Bytes(b) if integer_digits(facts, dest).is_some() && !matches!(src.map(|s| s.kind), Some(Kind::NumericEdited { .. })) => {
382                move_digit_halves(facts, unit, dest, &b, pos)?;
383            }
384            Val::Bytes(b) => {
385                let v = match src.map(|s| (s, s.kind)) {
386                    Some((s, Kind::NumericEdited { edit, .. })) => {
387                        let (syms, currency) = facts.edit(edit);
388                        let (negative, magnitude) = edit::de_edit(syms, &page.decode(&b), currency);
389                        scaled_up(fixed(negative, U256::from_u128(magnitude), places_of(s.kind)), scaling(facts, s))
390                    }
391                    _ => {
392                        let digits = &b[b.len().saturating_sub(MAX_DIGITS)..];
393                        zoned_value(facts.options().numproc, digits, false, None, Places::new(digits.len() as u32, 0), pos)?
394                    }
395                };
396                store_fixed(facts, unit, dest, &v, false, pos)?;
397            }
398            Val::National(_) => return Err(Abend::ironwork("a national value cannot be moved to a numeric item", pos)),
399            Val::Address(_) => return Err(Abend::ironwork("a pointer cannot be moved to a numeric item", pos)),
400        },
401        Kind::Float(p) => {
402            let h = match val {
403                Val::Float(h) if h.precision.digits() > p.digits() => float::narrow_rounded(h, p).map_err(|c| Abend::check(c, pos))?,
404                Val::Float(h) => h.lengthen(p),
405                Val::Num(f) => float::from_fixed(f, p, ProgramMask::default()).map_err(|c| Abend::check(c, pos))?,
406                Val::Fig(Figurative::Zero) => Hfp::zero(p),
407                _ => return Err(Abend::ironwork("this value cannot be moved to a floating-point item", pos)),
408            };
409            write(&mut unit.mem, dest, &h.to_bytes());
410        }
411    }
412    Ok(())
413}
414
415/// The digits of a zoned or packed integer item without P scaling.
416fn integer_digits(facts: &dyn ProgramFacts, loc: Loc) -> Option<usize> {
417    match loc.kind {
418        Kind::Zoned { digits, scale: 0, .. } | Kind::Packed { digits, scale: 0, .. } if scaling(facts, loc) == 0 => Some(digits as usize),
419        _ => None,
420    }
421}
422
423fn is_decimal(kind: Kind) -> bool {
424    matches!(kind, Kind::Zoned { .. } | Kind::Packed { .. })
425}
426
427/// NUMPROC(PFD), a signed packed item moved to one of the same kind and scaling: the bytes are copied.
428fn packed_copy(facts: &dyn ProgramFacts, src: Loc, dest: Loc) -> bool {
429    matches!(dest.kind, Kind::Packed { digits, signed: true, .. } if digits > 0)
430        && src.kind == dest.kind
431        && scaling(facts, src) == scaling(facts, dest)
432        && facts.options().numproc == Numproc::Pfd
433}
434
435/// Whether a MOVE from a zoned or packed sender compiles only to instructions that check no digit
436/// or sign: a byte copy, PACK, UNPK, and the OI that makes a sign F. A packed sender to another packed
437/// shape takes ZAP or SRP, a binary receiver CVB, a numeric-edited one ED, and those check
438/// (assumption C260).
439fn moved_unchecked(facts: &dyn ProgramFacts, src: Loc, dest: Loc) -> bool {
440    match (src.kind, dest.kind) {
441        (Kind::Packed { .. }, Kind::Packed { .. }) => packed_copy(facts, src, dest),
442        (Kind::Zoned { .. } | Kind::Packed { .. }, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Group) => true,
443        (Kind::Zoned { .. } | Kind::Packed { .. }, Kind::Alnum { .. } | Kind::AlnumEdited { .. }) => places_of(src.kind).dec == 0,
444        _ => false,
445    }
446}
447
448/// A MOVE's sending item read as a number; but where the MOVE checks nothing (C260) and its digits
449/// or sign are not decimal, its bytes as stored, after INVDATA(CLEANSIGN), which `assign` carries
450/// to the receiver unchecked.
451pub fn move_sender(facts: &dyn ProgramFacts, mem: &[u8], src: Loc, dest: Loc, pos: Pos) -> R<Val> {
452    match read(facts, mem, src, pos) {
453        Err(Abend { code: AbendCode::Check(ProgramCheck::Data), .. }) if moved_unchecked(facts, src, dest) => {
454            let stored = bytes(mem, src);
455            let cleaned = facts.options().invdata.is_some_and(|i| i.cleansign).then(|| sign_cleaned(stored, src.kind)).flatten();
456            Ok(Val::Bytes(cleaned.unwrap_or_else(|| stored.to_vec())))
457        }
458        value => value,
459    }
460}
461
462/// A zoned or packed item's digits as half-bytes, most significant first, and its sign half-byte
463/// when signed; SIGN SEPARATE gives D for '-' and C for any other character.
464fn digit_halves(kind: Kind, b: &[u8]) -> (Vec<u8>, Option<u8>) {
465    match kind {
466        Kind::Packed { digits, signed, .. } => {
467            let nibbles: Vec<u8> = b.iter().flat_map(|x| [x >> 4, x & 0x0F]).collect();
468            let (sign, body) = nibbles.split_last().expect("a packed item is at least one byte");
469            (body[body.len().saturating_sub(digits as usize)..].to_vec(), signed.then_some(*sign))
470        }
471        Kind::Zoned { sign: Some(SignClause { separate: true, position }), .. } => {
472            let (s, body) = if position == SignPosition::Leading { (b[0], &b[1..]) } else { (b[b.len() - 1], &b[..b.len() - 1]) };
473            (body.iter().map(|x| x & 0x0F).collect(), Some(if s == 0x60 { 0x0D } else { 0x0C }))
474        }
475        Kind::Zoned { signed, sign, .. } => {
476            let at = if matches!(sign, Some(SignClause { position: SignPosition::Leading, .. })) { 0 } else { b.len() - 1 };
477            (b.iter().map(|x| x & 0x0F).collect(), signed.then(|| b[at] >> 4))
478        }
479        _ => (Vec::new(), None),
480    }
481}
482
483/// A zoned or packed sender whose digits or sign are not decimal, moved to a zoned or packed
484/// receiver as PACK and UNPK move it: each receiver digit takes the sender's digit of the same
485/// power of ten, zero where there is none, and the data exception comes where the receiver is next
486/// read as a number (C260).
487fn carry_digits<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, dest: Loc, src: Loc, sender: &[u8], pos: Pos) -> R<()> {
488    if packed_copy(facts, src, dest) {
489        write(&mut unit.mem, dest, sender);
490        return Ok(());
491    }
492    let (halves, sign) = digit_halves(src.kind, sender);
493    let power = |loc: Loc| i64::from(scaling(facts, loc)) - i64::from(places_of(loc.kind).dec);
494    let (m, n) = (halves.len() as i64, dest.kind.digits_scale().map_or(0, |(d, _)| d) as i64);
495    let shift = m - n + power(src) - power(dest);
496    let aligned: Vec<u8> = (0..n).map(|j| usize::try_from(j + shift).ok().and_then(|i| halves.get(i)).copied().unwrap_or(0)).collect();
497    store_digit_halves(facts, unit, dest, &aligned, sign, pos)
498}
499
500/// An alphanumeric sender moved to a zoned or packed integer: the low half of each of its last bytes
501/// as a digit, zeros to the left, stored positive and unchecked. The byte copy, PACK and UNPK such a
502/// MOVE compiles to check no digit, so a non-digit is a data exception only where the item is next
503/// read as a number (assumption C240).
504fn move_digit_halves<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, dest: Loc, sender: &[u8], pos: Pos) -> R<()> {
505    let n = integer_digits(facts, dest).unwrap_or(0);
506    let halves: Vec<u8> = (0..n).map(|i| (sender.len() + i).checked_sub(n).map_or(0, |k| sender[k] & 0x0F)).collect();
507    store_digit_halves(facts, unit, dest, &halves, None, pos)
508}
509
510/// One digit half-byte per receiver digit, and the sender's sign half-byte: the value of the
511/// decimal digits is stored, then each half above 9 is put back in its digit's place, and a sign
512/// half that is a digit in a signed receiver's sign place. An unsigned receiver's sign is F.
513fn store_digit_halves<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, dest: Loc, halves: &[u8], sign: Option<u8>, pos: Pos) -> R<()> {
514    let n = halves.len();
515    let value = halves.iter().fold(0u128, |v, &h| v * 10 + if h > 9 { 0 } else { u128::from(h) });
516    let magnitude = U256::from_u128(value).checked_mul(pow10(scaling(facts, dest))).unwrap_or_default();
517    let negative = sign.is_some_and(|s| s > 9 && decimal::is_minus(s));
518    store_fixed(facts, unit, dest, &fixed(negative, magnitude, places(facts, dest)), false, pos)?;
519    let sign_digit = sign.filter(|&s| s <= 9);
520    if halves.iter().all(|&h| h <= 9) && sign_digit.is_none() {
521        return Ok(());
522    }
523    let mut out = bytes(&unit.mem, dest).to_vec();
524    for (i, &h) in halves.iter().enumerate() {
525        let (at, high) = match dest.kind {
526            Kind::Packed { .. } => {
527                let nibble = 2 * out.len() - 1 - n + i;
528                (nibble / 2, nibble.is_multiple_of(2))
529            }
530            Kind::Zoned { sign: Some(SignClause { separate: true, position: SignPosition::Leading }), .. } => (i + 1, false),
531            _ => (i, false),
532        };
533        out[at] = if high { (out[at] & 0x0F) | (h << 4) } else { (out[at] & 0xF0) | h };
534    }
535    if let Some(s) = sign_digit {
536        let last = out.len() - 1;
537        match dest.kind {
538            Kind::Packed { signed: true, .. } => out[last] = (out[last] & 0xF0) | s,
539            Kind::Zoned { signed: true, sign: Some(SignClause { separate: true, .. }), .. } => {}
540            Kind::Zoned { signed: true, sign: Some(SignClause { position: SignPosition::Leading, .. }), .. } => out[0] = (out[0] & 0x0F) | (s << 4),
541            Kind::Zoned { signed: true, .. } => out[last] = (out[last] & 0x0F) | (s << 4),
542            _ => {}
543        }
544    }
545    write(&mut unit.mem, dest, &out);
546    Ok(())
547}
548
549/// What an alphanumeric receiver gets from a zoned or packed integer whose digits or sign are not
550/// decimal: a zoned sender's digit bytes as stored, an overpunched sign's zone made F; a packed
551/// sender's digits unpacked with F zones; a zero for each P (C260).
552fn unchecked_digit_bytes(facts: &dyn ProgramFacts, src: Loc, b: &[u8]) -> Vec<u8> {
553    let mut out = match src.kind {
554        Kind::Zoned { sign: Some(SignClause { separate: true, position }), .. } => {
555            if position == SignPosition::Leading { b[1..].to_vec() } else { b[..b.len() - 1].to_vec() }
556        }
557        Kind::Zoned { signed: true, sign, .. } => {
558            let mut digits = b.to_vec();
559            let at = if matches!(sign, Some(SignClause { position: SignPosition::Leading, .. })) { 0 } else { digits.len() - 1 };
560            digits[at] |= 0xF0;
561            digits
562        }
563        Kind::Zoned { .. } => b.to_vec(),
564        _ => digit_halves(src.kind, b).0.iter().map(|h| 0xF0 | h).collect(),
565    };
566    out.resize(out.len() + scaling(facts, src) as usize, 0xF0);
567    out
568}
569
570/// The bytes an alphanumeric receiver of `len` gets from `val`.
571pub fn alnum_image(facts: &dyn ProgramFacts, val: &Val, src: Option<Loc>, len: usize, pos: Pos) -> R<Vec<u8>> {
572    Ok(match val {
573        Val::Bytes(b) => match src {
574            Some(s) if is_decimal(s.kind) => unchecked_digit_bytes(facts, s, b),
575            _ => b.clone(),
576        },
577        Val::All(b) => b.iter().copied().cycle().take(len.max(b.len())).collect(),
578        Val::Fig(f) => vec![facts.figurative(*f); len],
579        Val::Num(f) if f.places.dec == 0 => {
580            let digits = src.and_then(|s| s.kind.digits_scale().map(|(d, _)| d + scaling(facts, s))).unwrap_or(f.places.total());
581            zoned_digits(f.magnitude.to_u128().unwrap_or(0), digits as usize, decimal::UNSIGNED)
582        }
583        Val::National(_) => return Err(Abend::ironwork("a national value cannot be moved to an alphanumeric item", pos)),
584        _ => return Err(Abend::ironwork("only an integer numeric value can be moved to an alphanumeric item", pos)),
585    })
586}
587
588/// What NUMCHECK finds wrong with a sending item's data, or None (see [`numcheck_fault_in`]).
589pub fn numcheck_fault(facts: &dyn ProgramFacts, mem: &[u8], loc: Loc, as_integer: bool) -> Option<&'static str> {
590    numcheck_fault_in(&facts.options(), loc.kind, bytes(mem, loc), facts.lax_redefinition(loc.item), as_integer)
591}
592
593/// What NUMCHECK finds wrong with `stored`, an item of `kind`, or None: a zoned or packed item that
594/// is not NUMERIC, its sign half-byte cleaned first under INVDATA(CLEANSIGN), and under ZON(LAX) a
595/// zoned item's `lax` redefinition tolerated; an alphanumeric item moved to a numeric one
596/// (`as_integer`) that is not an unsigned integer's digits; or a binary item holding more digits
597/// than its PICTURE. COMP-5 is not checked, nor binary under TRUNC(BIN) with BIN(NOTRUNCBIN)
598/// (Programming Guide SC27-8714-03, pp. 388-391).
599pub fn numcheck_fault_in(options: &Options, kind: Kind, stored: &[u8], lax: Option<LaxRedefinition>, as_integer: bool) -> Option<&'static str> {
600    let check = options.numcheck?;
601    let cleaned = options.invdata.is_some_and(|i| i.cleansign).then(|| sign_cleaned(stored, kind)).flatten();
602    let b = cleaned.as_deref().unwrap_or(stored);
603    let lax = lax.filter(|_| check.zon.is_some_and(|z| z.lax));
604    let spaces = match lax {
605        Some(LaxRedefinition::LeadingSpaces(n)) => n as usize,
606        _ => 0,
607    };
608    let digit = |x: &u8| (0xF0..=0xF9).contains(x);
609    let digits = |from: usize, bytes: &[u8]| bytes.iter().enumerate().all(|(i, x)| digit(x) || from + i < spaces && *x == ebcdic::SPACE);
610    let overpunch = |at: usize, x: u8| matches!(x >> 4, 0xC | 0xD | 0xF) && x & 0x0F <= 9 || at < spaces && x == ebcdic::SPACE;
611    let valid = match kind {
612        Kind::Zoned { signed, sign, .. } if check.zon.is_some() => {
613            let last = b.len() - 1;
614            match (signed || lax == Some(LaxRedefinition::Signed), sign) {
615                (false, _) => digits(0, b),
616                (true, Some(SignClause { separate: true, position })) => {
617                    let (s, rest) = if position == SignPosition::Leading { (b[0], &b[1..]) } else { (b[last], &b[..last]) };
618                    matches!(s, 0x4E | 0x60) && rest.iter().all(digit)
619                }
620                (true, Some(SignClause { position: SignPosition::Leading, .. })) => overpunch(0, b[0]) && digits(1, &b[1..]),
621                (true, _) => overpunch(last, b[last]) && digits(0, &b[..last]),
622            }
623        }
624        Kind::Group | Kind::Alnum { .. } if as_integer && check.zon.is_some() => b.iter().all(digit),
625        Kind::Packed { digits, signed, .. } if check.pac => {
626            let spare_clear = digits % 2 == 1 || b[0] >> 4 == 0;
627            decimal::tp(b).is_ok_and(|cc| cc.0 == 0) && (signed || b[b.len() - 1] & 0x0F == 0x0F) && spare_clear
628        }
629        Kind::Binary { digits, signed, native: false, .. } if check.bin.is_some_and(|c| c.truncbin || options.trunc != Trunc::Bin) => {
630            let raw = Binary { digits: digits as u8, signed, native: true }.load(b);
631            raw.unsigned_abs() < 10u128.pow(digits)
632        }
633        _ => true,
634    };
635    (!valid).then_some(match kind {
636        Kind::Binary { .. } => "has more digits than its PICTURE allows",
637        _ => "is not NUMERIC",
638    })
639}
640
641/// NUMCHECK's run-time check of a sending item: under MSG a warning on the error stream, with the
642/// item, its bytes in hexadecimal, the line and the program, and the statement runs; under ABD a
643/// terminating message, U4038 (assumptions [`numeric::assumptions::NUMCHECK_SENDERS`] and
644/// [`numeric::assumptions::NUMCHECK_MESSAGE`]).
645pub fn numcheck<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, loc: Loc, as_integer: bool, program: &str, pos: Pos) -> R<()> {
646    if facts.numcheck_removed(loc.item, pos) {
647        return Ok(());
648    }
649    let Some(why) = numcheck_fault(facts, &unit.mem, loc, as_integer) else { return Ok(()) };
650    let message = format!("NUMCHECK: {} X'{}' in program {program} {why}", facts.item_name(loc.item), crate::digest::hex(bytes(&unit.mem, loc)).to_ascii_uppercase());
651    if facts.options().numcheck.is_some_and(|c| c.abd) {
652        return Err(Abend { code: crate::abend::AbendCode::user(4038), message, pos, file: None });
653    }
654    let _ = writeln!(unit.err, "ironwork: {pos}: {message}; the statement runs");
655    Ok(())
656}
657
658/// NUMERIC, ALPHABETIC, ALPHABETIC-LOWER or ALPHABETIC-UPPER, tested on an item's bytes.
659pub fn byte_class(facts: &dyn ProgramFacts, mem: &[u8], loc: Loc, test: ByteClass) -> bool {
660    let bytes = bytes(mem, loc);
661    match test {
662        ByteClass::Packed { signed } => decimal::tp(bytes).is_ok_and(|cc| cc.0 == 0) && (signed || bytes.last().is_some_and(|b| b & 0x0F == 0x0F)),
663        ByteClass::Zoned { signed } => bytes.iter().enumerate().all(|(i, &b)| {
664            let zone_ok = if i + 1 == bytes.len() && signed { matches!(b >> 4, 0xC | 0xD | 0xF) } else { b >> 4 == 0xF };
665            zone_ok && b & 0x0F <= 9
666        }),
667        ByteClass::Digits => bytes.iter().all(|b| (0xF0..=0xF9).contains(b)),
668        ByteClass::Alphabetic => bytes.iter().all(|&b| b == ebcdic::SPACE || facts.page().decode_byte(b).is_ascii_alphabetic()),
669        ByteClass::AlphabeticLower => bytes.iter().all(|&b| b == ebcdic::SPACE || facts.page().decode_byte(b).is_ascii_lowercase()),
670        ByteClass::AlphabeticUpper => bytes.iter().all(|&b| b == ebcdic::SPACE || facts.page().decode_byte(b).is_ascii_uppercase()),
671    }
672}
673
674/// POSITIVE, NEGATIVE or ZERO, tested on a value.
675pub fn sign_test(val: Val, test: SignTest, pos: Pos) -> R<bool> {
676    let v = match val {
677        Val::Num(f) => f,
678        Val::Float(h) => float::to_fixed(h, Places::new(31, 0), false).0,
679        _ => return Err(Abend::ironwork("a sign condition on a non-numeric operand", pos)),
680    };
681    Ok(match test {
682        SignTest::Positive => !v.negative && !v.magnitude.is_zero(),
683        SignTest::Negative => v.negative,
684        SignTest::Zero => v.magnitude.is_zero(),
685    })
686}
687
688/// Compares two operands, each a value and, for an item, its `Loc`: packed items under
689/// NUMPROC(PFD) as bytes, pointers as addresses, numbers algebraically, national as UTF-16, and
690/// anything else as alphanumeric in the program's collating sequence.
691pub fn compare(facts: &dyn ProgramFacts, mem: &[u8], a: (Val, Option<Loc>), b: (Val, Option<Loc>), pos: Pos) -> R<Ordering> {
692    let ((va, la), (vb, lb)) = (a, b);
693    if let (Some(x), Some(y)) = (la, lb)
694        && let (Kind::Packed { .. }, true, Numproc::Pfd) = (x.kind, x.kind == y.kind && scaling(facts, x) == scaling(facts, y), facts.options().numproc)
695    {
696        return sign::compare_packed(bytes(mem, x), bytes(mem, y), Numproc::Pfd).map_err(|c| Abend::check(c, pos));
697    }
698    let address = |v: &Val| match v {
699        Val::Address(a) => Some(*a),
700        Val::Fig(Figurative::Null) => Some(0),
701        _ => None,
702    };
703    if matches!(va, Val::Address(_)) || matches!(vb, Val::Address(_)) {
704        return match (address(&va), address(&vb)) {
705            (Some(x), Some(y)) => Ok(x.cmp(&y)),
706            _ => Err(Abend::ironwork("a pointer compared with something other than a pointer or NULL", pos)),
707        };
708    }
709    let numeric = |v: &Val| matches!(v, Val::Num(_) | Val::Float(_));
710    match (&va, &vb) {
711        (Val::Float(_), _) | (_, Val::Float(_)) if (numeric(&va) || matches!(va, Val::Fig(Figurative::Zero))) && (numeric(&vb) || matches!(vb, Val::Fig(Figurative::Zero))) => {
712            let to_float = |v: &Val| -> R<Hfp> {
713                Ok(match v {
714                    Val::Float(h) => h.lengthen(Precision::Extended),
715                    Val::Num(f) => float::from_fixed(*f, Precision::Extended, ProgramMask::default()).map_err(|c| Abend::check(c, pos))?,
716                    _ => Hfp::zero(Precision::Extended),
717                })
718            };
719            Ok(to_float(&va)?.compare(to_float(&vb)?))
720        }
721        (Val::Num(x), Val::Num(y)) => Ok(compare_fixed(x, y)),
722        (Val::Num(x), Val::Fig(Figurative::Zero)) => Ok(compare_fixed(x, &Fixed::new(0, Places::new(1, 0)))),
723        (Val::Fig(Figurative::Zero), Val::Num(y)) => Ok(compare_fixed(&Fixed::new(0, Places::new(1, 0)), y)),
724        (Val::National(x), Val::National(y)) => Ok(compare_national(x, y)),
725        _ => {
726            let (va, la) = stored_digits(facts, mem, va, la, pos)?;
727            let (vb, lb) = stored_digits(facts, mem, vb, lb, pos)?;
728            let len = image_len(&va, la).max(image_len(&vb, lb));
729            let x = alnum_image(facts, &va, la, len, pos)?;
730            let y = alnum_image(facts, &vb, lb, len, pos)?;
731            Ok(ebcdic::compare_alphanumeric(&x, &y, facts.collation()))
732        }
733    }
734}
735
736/// The bytes a zoned integer holds, as a comparison with a nonnumeric operand reads them: as a MOVE
737/// to an alphanumeric item of its size leaves them, so a sign it overpunches is removed under ZWB
738/// and kept under NOZWB, and a separate sign is left out (assumption C221). None for an operand the
739/// comparison reads as a number first: not zoned, not an integer, or scaled.
740pub fn compared_zoned_bytes(facts: &dyn ProgramFacts, mem: &[u8], loc: Loc) -> Option<Vec<u8>> {
741    let Kind::Zoned { scale: 0, signed, sign, .. } = loc.kind else { return None };
742    if scaling(facts, loc) > 0 {
743        return None;
744    }
745    let mut image = bytes(mem, loc).to_vec();
746    match sign {
747        Some(SignClause { separate: true, position: SignPosition::Leading }) => {
748            image.remove(0);
749        }
750        Some(SignClause { separate: true, position: SignPosition::Trailing }) => {
751            image.pop();
752        }
753        _ if !signed || !facts.options().zwb => {}
754        Some(SignClause { position: SignPosition::Leading, .. }) => image[0] |= 0xF0,
755        _ => *image.last_mut()? |= 0xF0,
756    }
757    Some(image)
758}
759
760/// `image`, from [`compared_zoned_bytes`], compared as alphanumeric with `other`, whose own bytes
761/// are taken the same way when it is a zoned integer too; `zoned_first` is whether the zoned item
762/// is the comparison's first operand.
763pub fn compare_zoned_bytes(facts: &dyn ProgramFacts, mem: &[u8], image: &[u8], other: (Val, Option<Loc>), zoned_first: bool, pos: Pos) -> R<Ordering> {
764    let len = image.len().max(image_len(&other.0, other.1));
765    let y = match other.1.and_then(|l| compared_zoned_bytes(facts, mem, l)) {
766        Some(bytes) => bytes,
767        None => alnum_image(facts, &other.0, other.1, len, pos)?,
768    };
769    let o = ebcdic::compare_alphanumeric(image, &y, facts.collation());
770    Ok(if zoned_first { o } else { o.reverse() })
771}
772
773/// A numeric operand compared with a nonnumeric one is its digits, scaling positions ignored
774/// (Language Reference SC27-8713-03, p. 211).
775pub fn stored_digits(facts: &dyn ProgramFacts, mem: &[u8], v: Val, loc: Option<Loc>, pos: Pos) -> R<(Val, Option<Loc>)> {
776    match loc {
777        Some(l) if matches!(v, Val::Num(_)) && scaling(facts, l) > 0 => Ok((read_stored(facts, mem, l, pos)?, None)),
778        _ => Ok((v, loc)),
779    }
780}
781
782/// How many characters an operand has when compared as alphanumeric.
783pub fn image_len(v: &Val, loc: Option<Loc>) -> usize {
784    match (v, loc) {
785        (_, Some(l)) if !l.kind.is_numeric() => l.len,
786        (Val::Bytes(b) | Val::All(b), _) => b.len(),
787        (Val::Num(f), Some(l)) => l.kind.digits_scale().map_or(f.places.total(), |(d, _)| d) as usize,
788        (Val::Num(f), None) => f.places.total() as usize,
789        _ => 1,
790    }
791}
792
793/// National values compared unit by unit, the shorter padded with national spaces.
794pub fn compare_national(a: &[u8], b: &[u8]) -> Ordering {
795    let unit = |s: &[u8], i: usize| if i + 1 < s.len() { u16::from_be_bytes([s[i], s[i + 1]]) } else { 0x0020 };
796    let len = a.len().max(b.len());
797    (0..len).step_by(2).map(|i| unit(a, i).cmp(&unit(b, i))).find(|o| o.is_ne()).unwrap_or(Ordering::Equal)
798}