Skip to main content

ironwork_rt/
codec.rs

1//! A numeric item's storage read as generated code reads it. The interpreter and the SQL runtime
2//! share it, so a host variable sends exactly the value a COMPUTE would read.
3
4use numeric::Numproc;
5use crate::vocab::{SignClause, SignPosition};
6use zarch::check::ProgramCheck;
7use zarch::decimal::{self, Decimal};
8
9/// The longest packed field DECIMAL instructions take, and the most digits a PACK of one gives.
10const PACKED_MAX: usize = 16;
11const ZONED_MAX: usize = 2 * PACKED_MAX - 1;
12/// The most digits, each at most 15 where the data is bad, whose value fits a `u64`.
13const U64_DIGITS: usize = 18;
14
15/// A packed field. Under NUMPROC(NOPFD) an unsigned field's sign nibble is forced to F first.
16pub fn packed(bytes: &[u8], signed: bool, numproc: Numproc) -> Result<Decimal, ProgramCheck> {
17    if signed || numproc != Numproc::Nopfd {
18        return decimal::decode(bytes);
19    }
20    let mut held = [0u8; PACKED_MAX];
21    let mut grown = Vec::new();
22    let p = match held.get_mut(..bytes.len()) {
23        Some(p) => p,
24        None => {
25            grown.extend_from_slice(bytes);
26            &mut grown[..]
27        }
28    };
29    p.copy_from_slice(bytes);
30    *p.last_mut().unwrap() |= 0x0F;
31    decimal::decode(p)
32}
33
34/// A zoned field, entering through PACK, which keeps only the sign's zone.
35pub fn zoned(bytes: &[u8], signed: bool, sign: Option<SignClause>, numproc: Numproc) -> Result<Decimal, ProgramCheck> {
36    if matches!(sign, None | Some(SignClause { separate: false, position: SignPosition::Trailing })) && (1..=ZONED_MAX).contains(&bytes.len()) {
37        return packed_zoned(bytes, signed, numproc);
38    }
39    let mut held = [0u8; ZONED_MAX + 1];
40    let mut grown = Vec::new();
41    let mut zoned = match held.get_mut(..bytes.len()) {
42        Some(z) => z,
43        None => {
44            grown.extend_from_slice(bytes);
45            &mut grown[..]
46        }
47    };
48    zoned.copy_from_slice(bytes);
49    let mut separate_negative = None;
50    match sign {
51        Some(SignClause { separate: true, position }) => {
52            let whole = std::mem::take(&mut zoned);
53            let (s, rest) = if position == SignPosition::Leading { whole.split_first_mut().unwrap() } else { whole.split_last_mut().unwrap() };
54            separate_negative = Some(match *s {
55                0x60 => true,
56                0x4E => false,
57                _ => return Err(ProgramCheck::Data),
58            });
59            zoned = rest;
60            *zoned.last_mut().unwrap() |= 0xF0;
61        }
62        Some(SignClause { separate: false, position: SignPosition::Leading }) => {
63            let zone = zoned[0] & 0xF0;
64            zoned[0] |= 0xF0;
65            let last = zoned.len() - 1;
66            zoned[last] = zone | (zoned[last] & 0x0F);
67        }
68        _ => {}
69    }
70    let value = if (1..=ZONED_MAX).contains(&zoned.len()) { packed_zoned(zoned, signed, numproc)? } else { packed(&numeric::zoned::pack(zoned)?, signed, numproc)? };
71    Ok(match separate_negative {
72        Some(negative) => Decimal { negative, ..value },
73        None => value,
74    })
75}
76
77/// What `packed` gives for PACK of 1 to 31 zoned bytes: their digit nibbles, after a zero nibble
78/// when there are evenly many, then the last zone as the sign.
79fn packed_zoned(zoned: &[u8], signed: bool, numproc: Numproc) -> Result<Decimal, ProgramCheck> {
80    let mut digit_bad = false;
81    let mut digit = |b: &u8| {
82        digit_bad |= b & 0x0F > 9;
83        b & 0x0F
84    };
85    let magnitude = if zoned.len() <= U64_DIGITS {
86        u128::from(zoned.iter().fold(0u64, |m, b| m * 10 + u64::from(digit(b))))
87    } else {
88        zoned.iter().fold(0u128, |m, b| m * 10 + u128::from(digit(b)))
89    };
90    let sign = if !signed && numproc == Numproc::Nopfd { 0x0F } else { zoned[zoned.len() - 1] >> 4 };
91    if digit_bad || !decimal::is_sign(sign) {
92        return Err(ProgramCheck::Data);
93    }
94    Ok(Decimal { negative: decimal::is_minus(sign), magnitude })
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn packed_by_copy(bytes: &[u8], signed: bool, numproc: Numproc) -> Result<Decimal, ProgramCheck> {
102        let mut p = bytes.to_vec();
103        if !signed && numproc == Numproc::Nopfd {
104            *p.last_mut().unwrap() |= 0x0F;
105        }
106        decimal::decode(&p)
107    }
108
109    fn zoned_by_pack(bytes: &[u8], signed: bool, sign: Option<SignClause>, numproc: Numproc) -> Result<Decimal, ProgramCheck> {
110        let mut zoned = bytes.to_vec();
111        let mut separate_negative = None;
112        match sign {
113            Some(SignClause { separate: true, position }) => {
114                let s = if position == SignPosition::Leading { zoned.remove(0) } else { zoned.pop().unwrap() };
115                separate_negative = Some(match s {
116                    0x60 => true,
117                    0x4E => false,
118                    _ => return Err(ProgramCheck::Data),
119                });
120                *zoned.last_mut().unwrap() |= 0xF0;
121            }
122            Some(SignClause { separate: false, position: SignPosition::Leading }) => {
123                let zone = zoned[0] & 0xF0;
124                zoned[0] |= 0xF0;
125                let last = zoned.len() - 1;
126                zoned[last] = zone | (zoned[last] & 0x0F);
127            }
128            _ => {}
129        }
130        let p = numeric::zoned::pack(&zoned)?;
131        let value = packed_by_copy(&p, signed, numproc)?;
132        Ok(match separate_negative {
133            Some(negative) => Decimal { negative, ..value },
134            None => value,
135        })
136    }
137
138    /// Bytes mostly of valid digits and signs, with every kind of fault mixed in, from xorshift64*.
139    fn fields(count: usize) -> Vec<Vec<u8>> {
140        let mut state = 0x2545_F491_4F6C_DD1Du64;
141        let mut next = move || {
142            state ^= state >> 12;
143            state ^= state << 25;
144            state ^= state >> 27;
145            state.wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 32
146        };
147        const ZONES: [u8; 8] = [0xF0, 0xF0, 0xF0, 0xC0, 0xD0, 0xB0, 0x40, 0x00];
148        const SEPARATE: [u8; 3] = [0x60, 0x4E, 0xF1];
149        (0..count)
150            .map(|_| {
151                let len = 1 + (next() % 36) as usize;
152                let mut field: Vec<u8> = (0..len).map(|_| if next() % 20 == 0 { next() as u8 } else { 0xF0 | (next() % 10) as u8 }).collect();
153                let at = if next() % 2 == 0 { 0 } else { len - 1 };
154                field[at] = ZONES[(next() % 8) as usize] | (field[at] & 0x0F);
155                if next() % 3 == 0 {
156                    field[at] = SEPARATE[(next() % 3) as usize];
157                }
158                field
159            })
160            .collect()
161    }
162
163    #[test]
164    fn zoned_and_packed_read_as_pack_then_decode_reads_them() {
165        let signs = [
166            None,
167            Some(SignClause { position: SignPosition::Trailing, separate: false }),
168            Some(SignClause { position: SignPosition::Leading, separate: false }),
169            Some(SignClause { position: SignPosition::Leading, separate: true }),
170            Some(SignClause { position: SignPosition::Trailing, separate: true }),
171        ];
172        let mut read = 0;
173        for field in fields(4000) {
174            for numproc in [Numproc::Nopfd, Numproc::Pfd] {
175                for signed in [false, true] {
176                    assert_eq!(packed(&field, signed, numproc), packed_by_copy(&field, signed, numproc), "packed {field:02X?}");
177                    for sign in signs {
178                        if sign.is_some_and(|s| s.separate) && field.len() < 2 {
179                            continue;
180                        }
181                        let value = zoned(&field, signed, sign, numproc);
182                        assert_eq!(value, zoned_by_pack(&field, signed, sign, numproc), "zoned {field:02X?} {sign:?}");
183                        read += usize::from(value.is_ok());
184                    }
185                }
186            }
187        }
188        assert!(read > 10_000, "only {read} fields were valid");
189    }
190}