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}