base/
splay.rs

1//! Splay/unsplay 36-bit words.
2//!
3//! Splayed mode (sometimes called "Assembly Mode") is described in:
4//!
5//! - The documentation for the Photoelectric Tape Reader (sequence 52)
6//!   in chapter 5 of the Users Handbook.
7//! - The documentation for the paper tape punch (sequence 63) in
8//!   chapter 5 of the Users Handbook, and in the tape diagram
9//!   immediately below that.
10//! - Note 15 accompanying the Instruction Execuition table on page
11//!   3-74 of the Users Handbook.
12//! - Section 4-3.7 ("TSD - TRANSFER DATA") of the Users Handbook.
13use std::ops::Shl;
14
15#[cfg(test)]
16use test_strategy::proptest;
17
18use super::onescomplement::unsigned::{Unsigned6Bit, Unsigned36Bit};
19#[cfg(test)]
20use super::u36;
21
22/// Loading a line of data from PETR in assembly mode has the effect
23/// of first cycling the target word left by one bit position, and
24/// then loading the incmoing bits into bit positions 0, 6, 12, 18,
25/// 24, 30 (couting from the least significant bit).  If you repeat
26/// that operation 6 times, you will load bits into all the positions
27/// in a word.  This funciton performs this operation once (i.e. loads
28/// 6 bits).
29#[must_use]
30pub fn cycle_and_splay(target: Unsigned36Bit, bits: Unsigned6Bit) -> Unsigned36Bit {
31    // The data goes into the following bit positions:
32    // bit 1 (0 counting from 0) goes to 1.1 = 1 <<  0 (dec)
33    // bit 2 (1 counting from 0) goes to 1.7 = 1 <<  6 (dec)
34    // bit 3 (2 counting from 0) goes to 2.4 = 1 << 12 (dec)
35    // bit 4 (3 counting from 0) goes to 3.1 = 1 << 18 (dec)
36    // bit 5 (4 counting from 0) goes to 3.7 = 1 << 24 (dec)
37    // bit 6 (5 counting from 0) goes to 4.4 = 1 << 30 (dec)
38    let src = u64::from(bits);
39    let lowest_bits = Unsigned36Bit::from(0o010_101_010_101_u32);
40    let newbits: u64 = (src & 1)
41        | ((src & 0o2) << 5)
42        | ((src & 0o4) << 10)
43        | ((src & 0o10) << 15)
44        | ((src & 0o20) << 20)
45        | ((src & 0o40) << 25);
46    (target.shl(1) & !lowest_bits) | newbits
47}
48
49#[test]
50fn test_cycle_and_splay() {
51    assert_eq!(cycle_and_splay(Unsigned36Bit::ZERO, Unsigned6Bit::ZERO), 0);
52
53    let ex: u64 = 1u64 << 0 | 1u64 << 6 | 1u64 << 12 | 1u64 << 18 | 1u64 << 24 | 1u64 << 30;
54    let expected: Unsigned36Bit = Unsigned36Bit::try_from(ex).expect("test data should be valid");
55    assert_eq!(
56        cycle_and_splay(Unsigned36Bit::ZERO, Unsigned6Bit::MAX),
57        expected
58    );
59
60    for bitpos in 0u8..=5u8 {
61        let input_bit = Unsigned6Bit::try_from(1u8 << bitpos).expect("valid test data");
62        let current: Unsigned36Bit = 2_u8.into();
63        let expected_output = Unsigned36Bit::try_from(1u64 << (bitpos * 6))
64            .expect("valid test data")
65            | (current << Unsigned36Bit::ONE);
66        assert_eq!(cycle_and_splay(current, input_bit), expected_output);
67    }
68}
69
70/// This function reverses the effect of perfoming six calls to
71/// `cycle_and_splay()`.
72#[must_use]
73pub fn unsplay(source: Unsigned36Bit) -> [Unsigned6Bit; 6] {
74    fn bits(
75        b0: Unsigned36Bit,
76        b1: Unsigned36Bit,
77        b2: Unsigned36Bit,
78        b3: Unsigned36Bit,
79        b4: Unsigned36Bit,
80        b5: Unsigned36Bit,
81    ) -> Unsigned6Bit {
82        // We want to treat the b0 case (i.e. 1) consistently with the
83        // others, so we turn off the bool_to_int_with_if lint here.
84        #[allow(clippy::bool_to_int_with_if)]
85        let result: u8 = (if b0 == 0 { 0 } else { 0o01 })
86            | (if b1 == 0 { 0 } else { 0o02 })
87            | (if b2 == 0 { 0 } else { 0o04 })
88            | (if b3 == 0 { 0 } else { 0o10 })
89            | (if b4 == 0 { 0 } else { 0o20 })
90            | (if b5 == 0 { 0 } else { 0o40 });
91        Unsigned6Bit::try_from(result).unwrap()
92    }
93
94    let getbit = |n: u32| -> Unsigned36Bit { source & (1 << n) };
95    [
96        bits(
97            getbit(5),
98            getbit(11),
99            getbit(17),
100            getbit(23),
101            getbit(29),
102            getbit(35),
103        ),
104        bits(
105            getbit(4),
106            getbit(10),
107            getbit(16),
108            getbit(22),
109            getbit(28),
110            getbit(34),
111        ),
112        bits(
113            getbit(3),
114            getbit(9),
115            getbit(15),
116            getbit(21),
117            getbit(27),
118            getbit(33),
119        ),
120        bits(
121            getbit(2),
122            getbit(8),
123            getbit(14),
124            getbit(20),
125            getbit(26),
126            getbit(32),
127        ),
128        bits(
129            getbit(1),
130            getbit(7),
131            getbit(13),
132            getbit(19),
133            getbit(25),
134            getbit(31),
135        ),
136        bits(
137            getbit(0),
138            getbit(6),
139            getbit(12),
140            getbit(18),
141            getbit(24),
142            getbit(30),
143        ),
144    ]
145}
146
147#[test]
148fn test_unsplay() {
149    const ZERO: Unsigned6Bit = Unsigned6Bit::ZERO;
150    const MAX: Unsigned6Bit = Unsigned6Bit::MAX;
151    assert_eq!(
152        unsplay(Unsigned36Bit::ZERO),
153        [ZERO, ZERO, ZERO, ZERO, ZERO, ZERO]
154    );
155    assert_eq!(
156        unsplay(u36!(0o777_777_777_777)),
157        [MAX, MAX, MAX, MAX, MAX, MAX]
158    );
159}
160
161#[cfg(test)]
162fn round_trip(input: Unsigned36Bit) -> Result<(), String> {
163    let unsplayed = unsplay(input);
164    dbg!(&input);
165    dbg!(&unsplayed);
166    let mut output = Unsigned36Bit::ZERO;
167    for (i, component) in unsplayed.into_iter().enumerate() {
168        let next = cycle_and_splay(output, component);
169        println!(
170            "round_trip: splay iteration {i}: value {component:03o} changed word from {output:012o} to {next:012o}",
171        );
172        output = next;
173    }
174    dbg!(&output);
175    if input == output {
176        Ok(())
177    } else {
178        Err(format!(
179            "mismatch: input word {input:012o} unsplayed to {unsplayed:?}, which splayed to {output:012o}, which doesn't match",
180        ))
181    }
182}
183
184// test_unsplay_splay_round_trip doesn't necessarily cover all these cases.
185#[test]
186fn test_round_trip_selected() {
187    for value in [
188        Unsigned36Bit::ZERO,
189        Unsigned36Bit::ONE,
190        Unsigned36Bit::from(0o2_u32),
191        Unsigned36Bit::from(0o4_u32),
192        Unsigned36Bit::from(0o3_u32),
193        Unsigned36Bit::from(0o7_u32),
194        Unsigned36Bit::from(0o77_u32),
195        Unsigned36Bit::from(0o7700_u32),
196        Unsigned36Bit::MAX,
197        Unsigned36Bit::from(0o23_574_373_u32),
198    ] {
199        if let Err(e) = round_trip(value) {
200            panic!("round_trip failed for {value:012o}: {e}");
201        }
202    }
203}
204
205// This property test doesn't necessarily cover all the cases in
206// test_round_trip_selected, so we can't delete that test.
207#[cfg(test)]
208#[proptest]
209fn test_unsplay_splay_round_trip(input: Unsigned36Bit) {
210    if let Err(e) = round_trip(input) {
211        panic!("round trip failed for {input:012o}: {e}");
212    }
213}
214
215#[test]
216fn test_round_trip_bitcycle() {
217    for bitpos in 0..36 {
218        let input: Unsigned36Bit = Unsigned36Bit::ONE.shl(bitpos);
219        if let Err(e) = round_trip(input) {
220            panic!("round_trip failed for {input:012o}: {e}");
221        }
222    }
223}