Skip to main content

ironwork_numeric/
binary.rs

1use crate::options::{Options, Trunc, TruncCheck};
2use zarch::wide::U256;
3
4/// Whether a binary item's value is limited by its bytes rather than its PICTURE, and how many
5/// bytes it has.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum Native {
8    /// USAGE BINARY, COMP or COMP-4: two, four or eight bytes as the PICTURE's digits need, the
9    /// value truncated as TRUNC says.
10    #[default]
11    No,
12    /// COMP-5: the same sizes, never truncated to the PICTURE.
13    Comp5,
14    /// GnuCOBOL's and Micro Focus's BINARY-CHAR under `--compliance extended`: one byte, never
15    /// truncated to its three digits.
16    BinaryChar,
17    /// Micro Focus's COMP-X under `--compliance extended`: the fewest bytes that hold the PICTURE's
18    /// digits, never truncated to them, and shown in them.
19    CompX,
20    /// PIC X(n) COMP-5 under `--compliance extended`: n bytes, unsigned, never truncated, and shown
21    /// in the digits that hold its whole value.
22    Comp5Bytes,
23}
24
25impl Native {
26    pub const fn is_native(self) -> bool {
27        !matches!(self, Native::No)
28    }
29
30    /// Whether DISPLAY, a MOVE to an alphanumeric item, JSON and XML show the digits that hold any
31    /// value of the item's bytes rather than its PICTURE's digits.
32    pub fn shows_whole(self, trunc: Trunc) -> bool {
33        match self {
34            Native::No => trunc == Trunc::Bin,
35            Native::CompX => false,
36            Native::Comp5 | Native::BinaryChar | Native::Comp5Bytes => true,
37        }
38    }
39}
40
41/// The digit positions Micro Focus gives a PIC X(n) COMP-X item, `n` from 1 to 8: the digits any
42/// value of its n bytes can hold in full.
43pub const fn alphanumeric_digits(n: u32) -> u8 {
44    [2, 4, 7, 9, 12, 14, 16, 19][n as usize - 1]
45}
46
47/// A USAGE BINARY, COMP or COMP-4 item, or COMP-5, BINARY-CHAR or COMP-X as `native` says.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct Binary {
50    pub digits: u8,
51    pub signed: bool,
52    pub native: Native,
53}
54
55impl Binary {
56    pub const fn bytes(self) -> usize {
57        match (self.native, self.digits) {
58            (Native::BinaryChar, _) => 1,
59            (Native::CompX | Native::Comp5Bytes, 0..=2) => 1,
60            (Native::CompX | Native::Comp5Bytes, 3..=4) => 2,
61            (Native::CompX | Native::Comp5Bytes, 5..=7) => 3,
62            (Native::CompX | Native::Comp5Bytes, 8..=9) => 4,
63            (Native::CompX | Native::Comp5Bytes, 10..=12) => 5,
64            (Native::CompX | Native::Comp5Bytes, 13..=14) => 6,
65            (Native::CompX | Native::Comp5Bytes, 15..=16) => 7,
66            (_, 0..=4) => 2,
67            (_, 5..=9) => 4,
68            _ => 8,
69        }
70    }
71
72    pub fn load(self, bytes: &[u8]) -> i128 {
73        assert_eq!(bytes.len(), self.bytes());
74        let raw = bytes.iter().fold(0u128, |acc, &b| acc << 8 | b as u128);
75        let bits = 8 * bytes.len() as u32;
76        if self.signed && raw >> (bits - 1) == 1 { raw as i128 - (1i128 << bits) } else { raw as i128 }
77    }
78
79    fn decimal_truncation(self, value: i128) -> i128 {
80        let cap = if self.digits <= 38 { U256::pow10(u32::from(self.digits)).lo } else { 10u128.pow(u32::from(self.digits)) };
81        let magnitude = value.unsigned_abs();
82        let kept = (if magnitude < cap { magnitude } else { magnitude % cap }) as i128;
83        if value < 0 { -kept } else { kept }
84    }
85
86    fn binary_truncation(self, value: i128) -> i128 {
87        let bits = 8 * self.bytes() as u32;
88        let raw = (value as u128) & ((1u128 << bits) - 1);
89        if self.signed && raw >> (bits - 1) == 1 { raw as i128 - (1i128 << bits) } else { raw as i128 }
90    }
91}
92
93/// A TRUNC(OPT) store whose value exceeds the PICTURE, where decimal and binary truncation give
94/// different results and the program depends on which one the generated code happens to use.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct TruncOptDivergence {
97    pub value: i128,
98    pub decimal: i128,
99    pub binary: i128,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct Stored {
104    pub bytes: Vec<u8>,
105    pub value: i128,
106    pub divergence: Option<TruncOptDivergence>,
107}
108
109/// Stores `value`, already scaled to the item's decimal places. An unsigned item takes the absolute
110/// value. See [`crate::assumptions::TRUNC_OPT_IS_BINARY`] for what TRUNC(OPT) keeps.
111pub fn store(item: Binary, value: i128, options: &Options) -> Stored {
112    let (kept, divergence) = kept(item, value, options);
113    Stored { bytes: kept.to_be_bytes()[16 - item.bytes()..].to_vec(), value: kept, divergence }
114}
115
116/// The value `store` keeps, whose low-order `item.bytes()` bytes it stores, and its divergence.
117pub fn kept(item: Binary, value: i128, options: &Options) -> (i128, Option<TruncOptDivergence>) {
118    let value = if item.signed { value } else { value.unsigned_abs() as i128 };
119    let decimal = item.decimal_truncation(value);
120    let binary = item.binary_truncation(value);
121    match (item.native.is_native(), options.trunc) {
122        (true, _) | (false, Trunc::Bin) => (binary, None),
123        (false, Trunc::Std) => (decimal, None),
124        (false, Trunc::Opt) => {
125            let report = decimal != binary && options.trunc_check == TruncCheck::Report;
126            (binary, report.then_some(TruncOptDivergence { value, decimal, binary }))
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::options::Options;
135
136    const HALFWORD: Binary = Binary { digits: 4, signed: false, native: Native::No };
137    const SIGNED_HALFWORD: Binary = Binary { digits: 4, signed: true, native: Native::No };
138
139    fn with(trunc: Trunc) -> Options {
140        Options { trunc, ..Options::default() }
141    }
142
143    #[test]
144    fn sizes_follow_the_picture() {
145        let size = |digits| Binary { digits, signed: true, native: Native::No }.bytes();
146        assert_eq!([size(1), size(4), size(5), size(9), size(10), size(18)], [2, 2, 4, 4, 8, 8]);
147    }
148
149    #[test]
150    fn std_truncates_to_the_picture() {
151        let s = store(HALFWORD, 12345, &with(Trunc::Std));
152        assert_eq!((s.value, s.bytes.as_slice()), (2345, [0x09, 0x29].as_slice()));
153        assert_eq!(store(SIGNED_HALFWORD, -12345, &with(Trunc::Std)).value, -2345);
154    }
155
156    #[test]
157    fn bin_truncates_to_the_halfword() {
158        assert_eq!(store(HALFWORD, 12345, &with(Trunc::Bin)).value, 12345);
159        assert_eq!(store(HALFWORD, 70000, &with(Trunc::Bin)).value, 70000 - 65536);
160        let s = store(SIGNED_HALFWORD, 40000, &with(Trunc::Bin));
161        assert_eq!((s.value, s.bytes.as_slice()), (40000 - 65536, [0x9C, 0x40].as_slice()));
162    }
163
164    #[test]
165    fn comp5_ignores_trunc() {
166        let native = Binary { native: Native::Comp5, ..HALFWORD };
167        assert_eq!(store(native, 12345, &with(Trunc::Std)).value, 12345);
168    }
169
170    #[test]
171    fn binary_char_is_one_byte_truncated_to_it() {
172        let byte = |signed| Binary { digits: 3, signed, native: Native::BinaryChar };
173        assert_eq!(byte(true).bytes(), 1);
174        let s = store(byte(true), -1, &with(Trunc::Std));
175        assert_eq!((s.value, s.bytes.as_slice()), (-1, [0xFF].as_slice()));
176        assert_eq!(store(byte(false), 300, &with(Trunc::Std)).value, 44);
177        assert_eq!(store(byte(true), 200, &with(Trunc::Std)).value, -56);
178        assert_eq!(byte(true).load(&[0x80]), -128);
179    }
180
181    #[test]
182    fn comp_x_takes_the_fewest_bytes_for_its_digits_and_keeps_what_they_hold() {
183        let comp_x = |digits| Binary { digits, signed: false, native: Native::CompX };
184        let sizes: Vec<usize> = (1..=18).map(|d| comp_x(d).bytes()).collect();
185        assert_eq!(sizes, [1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 7, 7, 8, 8]);
186        assert_eq!(store(comp_x(2), 250, &with(Trunc::Std)).value, 250);
187        assert_eq!(store(comp_x(2), 300, &with(Trunc::Std)).value, 44);
188        let pic_x = |n| Binary { digits: alphanumeric_digits(n), signed: false, native: Native::Comp5Bytes }.bytes();
189        assert_eq!((1..=7).map(pic_x).collect::<Vec<_>>(), [1, 2, 3, 4, 5, 6, 7]);
190    }
191
192    #[test]
193    fn unsigned_items_take_the_absolute_value() {
194        assert_eq!(store(HALFWORD, -42, &with(Trunc::Std)).value, 42);
195    }
196
197    #[test]
198    fn opt_reports_a_store_where_decimal_and_binary_truncation_disagree() {
199        let s = store(HALFWORD, 12345, &with(Trunc::Opt));
200        assert_eq!(s.divergence, Some(TruncOptDivergence { value: 12345, decimal: 2345, binary: 12345 }));
201        assert_eq!(store(HALFWORD, 9999, &with(Trunc::Opt)).divergence, None);
202    }
203
204    #[test]
205    fn silent_stores_the_same_value_without_a_report() {
206        let mut options = with(Trunc::Opt);
207        options.apply_flag("-silent").unwrap();
208        let s = store(HALFWORD, 12345, &options);
209        assert_eq!((s.value, s.divergence), (12345, None));
210    }
211
212    #[test]
213    fn load_reads_twos_complement() {
214        assert_eq!(SIGNED_HALFWORD.load(&[0xFF, 0xFE]), -2);
215        assert_eq!(HALFWORD.load(&[0xFF, 0xFE]), 65534);
216    }
217}