t27.aiРусский

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.

Open the interactive lesson →

The full adder: a carry comes in, a carry goes out
The full adder: a carry comes in, a carry goes out ↗

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

Open the lesson's spec in the player ↗

All lessons