The full adder
You will learn
Why three trits still fit one sum trit and one carry trit.
A full adder adds a carry from the place below. Three trits sum to -3..3, and that range is always 3 x carry + sum with both still single trits; the spec's test checks all 27 cases. The lesson spec in the player is a different machine: ternary_full_adder.t27 builds a binary full adder out of ternary neurons, the way the repository's FPGA BitNet work does. Same word, two meanings, both runnable.
Try it
Set all three inputs to +1 and then to -1; read the carry and the sum, and find a case whose carry is 0 though no input is 0.

Add a carry from the place below. Three trits sum to -3..3, still one sum trit and one carry trit: 27 cases, every one shown.
specs/ternary/ternary_full_adder.t27
module TernaryFullAdder;
// Ternary primitives (binary values embedded as {0 -> N=-1, 1 -> P=+1}).
fn tmul(ta: u8, tb: u8) -> i8 {
if (ta == 1) { return 0; }
if (tb == 1) { return 0; }
if (ta == tb) { return 1; }
return -1;
}
fn dot27(a: u64, b: u64) -> i16 {
var acc : i16 = 0;
var i : u32 = 0;
while (i < 27) {
var ta : u8 = ((a >> (i << 1)) & 3) as u8;
var tb : u8 = ((b >> (i << 1)) & 3) as u8;
acc = acc + tmul(ta, tb) as i16;
i = i + 1;
}
return acc;
}
fn sign0(v: i16) -> u8 { if (v > 0) { return 2; } if (v < 0) { return 0; } return 1; }
fn negate(t: u8) -> u8 { if (t == 2) { return 0; } if (t == 0) { return 2; } return 1; }
fn pack2(t0: u8, t1: u8) -> u64 {
var z : u64 = 6004799503160661;
var cleared : u64 = z & 18446744073709551600;
return cleared | (t0 as u64) | ((t1 as u64) << 2);
}
fn pack3(t0: u8, t1: u8, t2: u8) -> u64 {
var z : u64 = 6004799503160661;
var cleared : u64 = z & 18446744073709551552;
return cleared | (t0 as u64) | ((t1 as u64) << 2) | ((t2 as u64) << 4);
}
fn bneuron(x: u64, w: u64, bias: i16) -> u8 { return sign0(dot27(x, w) + bias); }
// XOR of two binary-embedded trits: a 2-layer network (not linearly separable).
fn xor2(a: u8, b: u8) -> u8 {
var x : u64 = pack2(a, b);
var w : u64 = pack2(2, 2);
var h1 : u8 = bneuron(x, w, -1);
var h2 : u8 = bneuron(x, w, 1);
return bneuron(pack2(h2, negate(h1)), w, -1);
}
// Majority of three trits = sign(a+b+c): a single neuron with all-+1 weights.
fn maj3(a: u8, b: u8, c: u8) -> u8 {
return sign0(dot27(pack3(a, b, c), 12009599006321322));
}
// Binary full adder over trit-embedded bits. sum = a XOR b XOR cin (composed
// from the 2-layer XOR); carry = majority(a, b, cin) (a single neuron). Output
// packs the two result trits: sum in bits[1:0], carry in bits[3:2]. A real
// arithmetic building block built entirely from the spec-first ternary stack.
pub fn full_adder(a: u8, b: u8, cin: u8) -> u8 {
var s : u8 = xor2(xor2(a, b), cin);
var carry : u8 = maj3(a, b, cin);
return (carry << 2) | s;
}
test fa_000 { assert_eq(full_adder(0, 0, 0), 0); }
test fa_100 { assert_eq(full_adder(2, 0, 0), 2); }
test fa_110 { assert_eq(full_adder(2, 2, 0), 8); }
test fa_111 { assert_eq(full_adder(2, 2, 2), 10); }
test fa_011 { assert_eq(full_adder(0, 2, 2), 8); }
test fa_101 { assert_eq(full_adder(2, 0, 2), 8); }
// W697: the hardware boundary, derived from the CALL GRAPH.
//
// This spec has several functions that take a parameter and return a value,
// so W696's count rule left it AMBIGUOUS. But exactly ONE of them is called
// by no other function -- it is the root, and every other candidate is a
// helper it reaches. With one root the choice is forced by structure rather
// than by count, and forwarding to it still invents nothing.
//
// The call graph is built from FUNCTION BODIES ONLY. Counting `test` blocks
// as callers makes the rule vacuous -- every function is called by its own
// test, so nothing is ever a root.
//
// Measured: the rule resolved 14 of 136 ambiguous specs, 6 of them with
// types that can cross a module boundary. It is narrow because most of the
// rest are libraries of INDEPENDENT functions, which have several roots and
// correctly stay ambiguous.
fn on_comb(a: u8, b: u8, cin: u8) -> u8 { return full_adder(a, b, cin); }
endmodule
All lessons
Module 1 · Three values
Why three, how balanced ternary writes every number without a sign, and what flipping and cutting trits do.
Module 2 · Logic with unknown
Kleene's three-valued gates, two gates binary has no twin for, and addition as a pair of tables.
Module 3 · Adding trits
The half adder, the full adder and a carry that ripples left, one place at a time.
Module 4 · Multiplying without a multiplier
Copy, drop or flip: a product by one trit, long multiplication, and a MAC that only adds.
Module 5 · Trits in binary memory
Two bits per trit, five trits per byte, and the bits a 27-trit word needs.
Module 6 · The TRI-27 instruction word
The 32-bit word the Trinity emulator decodes: its fields, its 47 opcodes and its 15-bit immediate.
Module 7 · A ternary machine
An ALU built from this course's adders, a three-way jump, and a program you can step.
Module 8 · Three answers
Compare at the highest differing trit, find a number in thirds, sort with three-way compares.
Module 9 · Ternary neurons
Weights of -1, 0, +1: one neuron, a detector and a small layer, with no multiplier anywhere.