Skip to main content

ternaria_arith/
trit.rs

1//! The balanced ternary digit.
2
3use core::fmt;
4use core::ops::Neg;
5
6/// A balanced ternary digit: -1, 0, or +1 (D-01).
7///
8/// Discriminants are the numeric values, so `trit as i8` gives the value.
9/// Under the convention in D-06 the same values serve as three-valued logic
10/// constants:
11///
12/// | Trit | Number | Logic |
13/// |------|--------|-------|
14/// | [`Trit::Neg`]  | -1 | false |
15/// | [`Trit::Zero`] |  0 | unknown |
16/// | [`Trit::Pos`]  | +1 | true |
17///
18/// `unknown` means indeterminate: a value exists but is not known. It is not
19/// an error channel. See D-06.
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
21#[repr(i8)]
22pub enum Trit {
23    /// -1. Logical false.
24    Neg = -1,
25    /// 0. Logical unknown.
26    #[default]
27    Zero = 0,
28    /// +1. Logical true.
29    Pos = 1,
30}
31
32impl Trit {
33    /// Every trit, ascending.
34    pub const ALL: [Trit; 3] = [Trit::Neg, Trit::Zero, Trit::Pos];
35
36    /// The numeric value, -1, 0, or +1.
37    #[inline]
38    pub const fn value(self) -> i8 {
39        self as i8
40    }
41
42    /// Builds a trit from its numeric value. `None` outside -1..=1.
43    #[inline]
44    pub const fn from_value(v: i8) -> Option<Trit> {
45        match v {
46            -1 => Some(Trit::Neg),
47            0 => Some(Trit::Zero),
48            1 => Some(Trit::Pos),
49            _ => None,
50        }
51    }
52
53    // ---- BCT encoding (D-03) ----
54    //
55    // Two bits per trit. The codes form a 2-bit two's-complement integer, so
56    // negation is arithmetic negation and the host can sign-extend a code
57    // directly.
58    //
59    //   00 -> 0     01 -> +1     11 -> -1     10 -> NaT (invalid)
60    //
61    // The spare 10 codepoint marks poisoned memory on the host. It is never
62    // visible to a guest program (D-03).
63
64    /// The 2-bit BCT code for this trit.
65    #[inline]
66    pub const fn bct_code(self) -> u8 {
67        match self {
68            Trit::Zero => 0b00,
69            Trit::Pos => 0b01,
70            Trit::Neg => 0b11,
71        }
72    }
73
74    /// Decodes a 2-bit BCT code. `None` for the `10` NaT pattern.
75    #[inline]
76    pub const fn from_bct_code(code: u8) -> Option<Trit> {
77        match code & 0b11 {
78            0b00 => Some(Trit::Zero),
79            0b01 => Some(Trit::Pos),
80            0b11 => Some(Trit::Neg),
81            _ => None, // 0b10 == NaT
82        }
83    }
84
85    /// The NaT (not-a-trit) code. Host-side poison; never visible to a guest.
86    pub const NAT_CODE: u8 = 0b10;
87
88    // ---- Kleene three-valued logic (D-06) ----
89    //
90    // NOT is negation, AND is min, OR is max.
91
92    /// Logical NOT - arithmetic negation. Swaps true/false, fixes unknown.
93    #[inline]
94    pub const fn not(self) -> Trit {
95        match self {
96            Trit::Neg => Trit::Pos,
97            Trit::Zero => Trit::Zero,
98            Trit::Pos => Trit::Neg,
99        }
100    }
101
102    /// Logical AND - the minimum of the two.
103    ///
104    /// Kleene semantics absorb unknown where the result is determined
105    /// regardless: `unknown AND false == false`, since the conjunction is
106    /// false for either value the unknown could take.
107    #[inline]
108    pub const fn and(self, other: Trit) -> Trit {
109        if (self as i8) <= (other as i8) {
110            self
111        } else {
112            other
113        }
114    }
115
116    /// Logical OR - the maximum of the two.
117    #[inline]
118    pub const fn or(self, other: Trit) -> Trit {
119        if (self as i8) >= (other as i8) {
120            self
121        } else {
122            other
123        }
124    }
125
126    /// Cyclic successor, -1 -> 0 -> +1 -> -1.
127    ///
128    /// {MIN, MAX, NEG} is not functionally complete. All three respect the
129    /// ordering -1 < 0 < +1, so no composition of them produces a map that
130    /// does not. This one does, and adding it completes the set (D-06).
131    #[inline]
132    pub const fn cycle(self) -> Trit {
133        match self {
134            Trit::Neg => Trit::Zero,
135            Trit::Zero => Trit::Pos,
136            Trit::Pos => Trit::Neg,
137        }
138    }
139
140    /// The Webb function, `V(x, y) = max(x, y) + 1 (mod 3)`.
141    ///
142    /// The ternary analogue of NAND: functionally complete on its own, so every
143    /// one of the 19,683 two-input ternary operations is a composition of this
144    /// single one (Post, 1941).
145    ///
146    /// # Completeness
147    ///
148    /// `webb_generates_every_unary_function` in this module verifies this by
149    /// construction: closing V under composition reaches all 27 unary ternary
150    /// functions, including the three constants. Two of them:
151    ///
152    /// ```text
153    /// cycle(x) = V(x, x)
154    /// NOT(x)   = V( V(V(x,x), V(x,V(x,x))),
155    ///               V( V(x,V(x,V(x,x))), V(V(x,x), V(x,V(x,x))) ) )
156    /// ```
157    ///
158    /// Cost differs sharply: `cycle` is one gate, `NOT` is seven, and
159    /// [`Trit::not`] is one operation. Webb is a foundation rather than an
160    /// implementation strategy. See D-13 in the roadmap.
161    #[inline]
162    pub const fn webb(self, other: Trit) -> Trit {
163        self.or(other).cycle()
164    }
165
166    /// Half adder: the sum trit and carry trit of `self + other`.
167    ///
168    /// The sum of two trits lies in -2..=2 and needs two trits to hold, hence
169    /// the carry. There is no separate borrow: subtraction is addition of the
170    /// negation.
171    ///
172    /// ```
173    /// use ternaria_arith::Trit;
174    /// // 1 + 1 = 2, which in balanced ternary is 1T: carry 1, sum -1.
175    /// let (sum, carry) = Trit::Pos.half_add(Trit::Pos);
176    /// assert_eq!((sum, carry), (Trit::Neg, Trit::Pos));
177    /// ```
178    #[inline]
179    pub const fn half_add(self, other: Trit) -> (Trit, Trit) {
180        Self::split_sum(self as i8 + other as i8)
181    }
182
183    /// Full adder: sum and carry of `self + other + carry_in`.
184    ///
185    /// The input sum spans -3..=3 and resolves into one sum trit and one
186    /// carry trit.
187    #[inline]
188    pub const fn full_add(self, other: Trit, carry_in: Trit) -> (Trit, Trit) {
189        Self::split_sum(self as i8 + other as i8 + carry_in as i8)
190    }
191
192    /// Splits a small integer into (sum trit, carry trit) with
193    /// `value == sum + 3 * carry`. Valid for -4..=4.
194    #[inline]
195    const fn split_sum(value: i8) -> (Trit, Trit) {
196        let (s, c) = match value {
197            -4 => (-1, -1),
198            -3 => (0, -1),
199            -2 => (1, -1),
200            -1 => (-1, 0),
201            0 => (0, 0),
202            1 => (1, 0),
203            2 => (-1, 1),
204            3 => (0, 1),
205            4 => (1, 1),
206            _ => panic!("split_sum: value out of range"),
207        };
208        (
209            match s {
210                -1 => Trit::Neg,
211                1 => Trit::Pos,
212                _ => Trit::Zero,
213            },
214            match c {
215                -1 => Trit::Neg,
216                1 => Trit::Pos,
217                _ => Trit::Zero,
218            },
219        )
220    }
221
222    /// The display character: `T` for -1, `0`, `1`.
223    ///
224    /// `T` is the conventional balanced-ternary notation for the -1 digit,
225    /// keeping every digit one character wide so a numeral reads like any other
226    /// positional numeral.
227    ///
228    /// # Counting from -9 to 9
229    ///
230    /// Worth reading down the middle column: the negative half is the positive
231    /// half with every digit flipped, because negation is digit flipping.
232    /// There is no sign to carry around, and no gap or asymmetry at either end.
233    ///
234    /// | n | balanced ternary | | n | balanced ternary |
235    /// |---:|:---|---|---:|:---|
236    /// | -9 | `T00` | | 9 | `100` |
237    /// | -8 | `T01` | | 8 | `10T` |
238    /// | -7 | `T1T` | | 7 | `1T1` |
239    /// | -6 | `T10` | | 6 | `1T0` |
240    /// | -5 | `T11` | | 5 | `1TT` |
241    /// | -4 | `TT`  | | 4 | `11`  |
242    /// | -3 | `T0`  | | 3 | `10`  |
243    /// | -2 | `T1`  | | 2 | `1T`  |
244    /// | -1 | `T`   | | 1 | `1`   |
245    /// | 0 | `0` | | | |
246    ///
247    /// Note `2` is `1T` - one three, minus one - rather than needing a digit
248    /// worth two. Every value has exactly one such representation.
249    #[inline]
250    pub const fn to_char(self) -> char {
251        match self {
252            Trit::Neg => 'T',
253            Trit::Zero => '0',
254            Trit::Pos => '1',
255        }
256    }
257
258    /// Parses a display character. Accepts `T`/`t`/`-` for -1 and `+` for +1.
259    #[inline]
260    pub const fn from_char(c: char) -> Option<Trit> {
261        match c {
262            'T' | 't' | '-' => Some(Trit::Neg),
263            '0' => Some(Trit::Zero),
264            '1' | '+' => Some(Trit::Pos),
265            _ => None,
266        }
267    }
268}
269
270impl Neg for Trit {
271    type Output = Trit;
272    /// Negation is the logical NOT - one operation serving both roles (D-01).
273    #[inline]
274    fn neg(self) -> Trit {
275        self.not()
276    }
277}
278
279impl fmt::Display for Trit {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str(match self {
282            Trit::Neg => "T",
283            Trit::Zero => "0",
284            Trit::Pos => "1",
285        })
286    }
287}
288
289impl From<Trit> for i8 {
290    #[inline]
291    fn from(t: Trit) -> i8 {
292        t as i8
293    }
294}
295
296impl From<Trit> for i64 {
297    #[inline]
298    fn from(t: Trit) -> i64 {
299        t as i8 as i64
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn value_round_trips() {
309        for t in Trit::ALL {
310            assert_eq!(Trit::from_value(t.value()), Some(t));
311        }
312        assert_eq!(Trit::from_value(2), None);
313        assert_eq!(Trit::from_value(-2), None);
314    }
315
316    #[test]
317    fn bct_round_trips_and_rejects_nat() {
318        for t in Trit::ALL {
319            assert_eq!(Trit::from_bct_code(t.bct_code()), Some(t));
320        }
321        assert_eq!(Trit::from_bct_code(Trit::NAT_CODE), None);
322    }
323
324    /// The BCT codes are 2-bit two's complement, which is what makes negation
325    /// on the host a plain arithmetic negate (D-03).
326    #[test]
327    fn bct_code_is_two_bit_twos_complement() {
328        for t in Trit::ALL {
329            // Sign-extend the 2-bit code into an i8 and recover the value.
330            let sign_extended = ((t.bct_code() << 6) as i8) >> 6;
331            assert_eq!(sign_extended, t.value(), "trit {t:?}");
332        }
333    }
334
335    #[test]
336    fn negation_is_an_involution() {
337        for t in Trit::ALL {
338            assert_eq!(-(-t), t);
339            assert_eq!((-t).value(), -t.value());
340        }
341    }
342
343    #[test]
344    fn kleene_truth_tables() {
345        use Trit::{Neg as F, Pos as T, Zero as U};
346
347        // AND is min.
348        assert_eq!(T.and(T), T);
349        assert_eq!(T.and(F), F);
350        assert_eq!(F.and(F), F);
351        assert_eq!(U.and(T), U);
352        assert_eq!(U.and(U), U);
353        // The absorbing case: determined regardless of what the unknown is.
354        assert_eq!(U.and(F), F);
355
356        // OR is max.
357        assert_eq!(F.or(F), F);
358        assert_eq!(T.or(F), T);
359        assert_eq!(U.or(F), U);
360        // Absorbing the other way.
361        assert_eq!(U.or(T), T);
362
363        // NOT fixes unknown.
364        assert_eq!(T.not(), F);
365        assert_eq!(F.not(), T);
366        assert_eq!(U.not(), U);
367    }
368
369    #[test]
370    fn and_or_really_are_min_max() {
371        for a in Trit::ALL {
372            for b in Trit::ALL {
373                assert_eq!(a.and(b).value(), a.value().min(b.value()));
374                assert_eq!(a.or(b).value(), a.value().max(b.value()));
375            }
376        }
377    }
378
379    #[test]
380    fn cycle_has_order_three() {
381        for t in Trit::ALL {
382            assert_eq!(t.cycle().cycle().cycle(), t);
383        }
384        // And it is genuinely order-breaking - the property that makes
385        // {min, max, neg} incomplete without it.
386        assert_ne!(Trit::Neg.cycle(), Trit::Neg);
387    }
388
389    /// {MIN, MAX, NEG} preserve the ordering -1 < 0 < +1, so no composition of
390    /// them can realise the cyclic successor. This test pins that argument down
391    /// by exhaustive search over every unary function built from them.
392    #[test]
393    fn min_max_neg_cannot_express_cycle() {
394        // Every function reachable from {min, max, neg} is monotone or
395        // antitone. `cycle` is neither: it maps Neg->Zero (up) but Pos->Neg
396        // (down), so it cannot be built from order-respecting parts.
397        let up = Trit::Neg.cycle().value() > Trit::Neg.value();
398        let down = Trit::Pos.cycle().value() < Trit::Pos.value();
399        assert!(
400            up && down,
401            "cycle must break monotonicity in both directions"
402        );
403    }
404
405    #[test]
406    fn webb_is_max_then_cycle() {
407        for a in Trit::ALL {
408            for b in Trit::ALL {
409                assert_eq!(a.webb(b), a.or(b).cycle());
410            }
411        }
412    }
413
414    /// Functional completeness, demonstrated rather than asserted: closing the
415    /// Webb function under composition reaches every unary ternary
416    /// function - all 27, the three constants included.
417    #[test]
418    fn webb_generates_every_unary_function() {
419        use std::collections::HashSet;
420
421        // A unary function is fully described by its image on (Neg, Zero, Pos).
422        type Fn3 = [Trit; 3];
423        let identity: Fn3 = [Trit::Neg, Trit::Zero, Trit::Pos];
424
425        let mut reached: HashSet<Fn3> = HashSet::new();
426        reached.insert(identity);
427
428        loop {
429            let before = reached.len();
430            let snapshot: Vec<Fn3> = reached.iter().copied().collect();
431            for f in &snapshot {
432                for g in &snapshot {
433                    let mut h = [Trit::Zero; 3];
434                    for i in 0..3 {
435                        h[i] = f[i].webb(g[i]);
436                    }
437                    reached.insert(h);
438                }
439            }
440            if reached.len() == before {
441                break;
442            }
443        }
444
445        assert_eq!(
446            reached.len(),
447            27,
448            "Webb must generate all 3^3 unary functions"
449        );
450
451        // The constants - the very things {min, max, neg} cannot produce.
452        for c in Trit::ALL {
453            assert!(reached.contains(&[c; 3]), "constant {c:?} unreachable");
454        }
455        // And NOT: Neg->Pos, Zero->Zero, Pos->Neg.
456        assert!(reached.contains(&[Trit::Pos, Trit::Zero, Trit::Neg]));
457    }
458
459    /// The explicit seven-gate Webb expression for NOT quoted in the docs.
460    ///
461    /// Kept as a test so the documented formula cannot rot. The cost contrast
462    /// is the lesson: `cycle` is one Webb gate, `NOT` is seven, and
463    /// [`Trit::not`] is one instruction.
464    #[test]
465    fn documented_webb_not_formula_holds() {
466        for x in Trit::ALL {
467            let a = x.webb(x); // cycle(x)
468            let b = x.webb(a);
469            let left = a.webb(b);
470            let c = x.webb(b);
471            let right = c.webb(left);
472            assert_eq!(left.webb(right), x.not(), "x = {x:?}");
473        }
474    }
475
476    #[test]
477    fn half_add_matches_arithmetic() {
478        for a in Trit::ALL {
479            for b in Trit::ALL {
480                let (sum, carry) = a.half_add(b);
481                assert_eq!(
482                    sum.value() as i32 + 3 * carry.value() as i32,
483                    a.value() as i32 + b.value() as i32,
484                    "{a:?} + {b:?}"
485                );
486            }
487        }
488    }
489
490    #[test]
491    fn full_add_matches_arithmetic() {
492        for a in Trit::ALL {
493            for b in Trit::ALL {
494                for c in Trit::ALL {
495                    let (sum, carry) = a.full_add(b, c);
496                    assert_eq!(
497                        sum.value() as i32 + 3 * carry.value() as i32,
498                        a.value() as i32 + b.value() as i32 + c.value() as i32,
499                        "{a:?} + {b:?} + {c:?}"
500                    );
501                }
502            }
503        }
504    }
505
506    /// A ripple-carry adder built from [`Trit::full_add`] must agree with plain
507    /// integer arithmetic - the check that the carry rule is right.
508    #[test]
509    fn ripple_carry_adder_over_four_trits() {
510        fn ripple(a: [Trit; 4], b: [Trit; 4]) -> (i32, Trit) {
511            let mut carry = Trit::Zero;
512            let mut out = [Trit::Zero; 4];
513            for i in 0..4 {
514                let (s, c) = a[i].full_add(b[i], carry);
515                out[i] = s;
516                carry = c;
517            }
518            let v = out
519                .iter()
520                .enumerate()
521                .map(|(i, t)| t.value() as i32 * 3i32.pow(i as u32))
522                .sum();
523            (v, carry)
524        }
525
526        fn value(t: [Trit; 4]) -> i32 {
527            t.iter()
528                .enumerate()
529                .map(|(i, t)| t.value() as i32 * 3i32.pow(i as u32))
530                .sum()
531        }
532
533        // 3^4 = 81 values each; exhaustive over all 6,561 pairs.
534        let all: Vec<[Trit; 4]> = (0..81)
535            .map(|n: i32| {
536                let mut v = n - 40; // centre on zero: -40..=40
537                let mut t = [Trit::Zero; 4];
538                for slot in t.iter_mut() {
539                    let mut r = v % 3;
540                    v /= 3;
541                    if r == 2 {
542                        r = -1;
543                        v += 1;
544                    } else if r == -2 {
545                        r = 1;
546                        v -= 1;
547                    }
548                    *slot = Trit::from_value(r as i8).unwrap();
549                }
550                t
551            })
552            .collect();
553
554        for a in &all {
555            for b in &all {
556                let (sum, carry) = ripple(*a, *b);
557                let expected = value(*a) + value(*b);
558                assert_eq!(
559                    sum + 81 * carry.value() as i32,
560                    expected,
561                    "{:?} + {:?}",
562                    value(*a),
563                    value(*b)
564                );
565            }
566        }
567    }
568
569    #[test]
570    fn char_round_trips() {
571        for t in Trit::ALL {
572            assert_eq!(Trit::from_char(t.to_char()), Some(t));
573        }
574        assert_eq!(Trit::from_char('-'), Some(Trit::Neg));
575        assert_eq!(Trit::from_char('+'), Some(Trit::Pos));
576        assert_eq!(Trit::from_char('2'), None);
577    }
578}