velxio/frontend/src/__tests__/digitalgate-kernel.test.ts

309 lines
12 KiB
TypeScript

/**
* digital-gate-engine Phase 0 — a gate network settles correctly on the
* multichip-bus kernel, with NO ngspice (project/digital-gate-engine/).
*
* Proves the event-driven settle kernel built for chip-to-chip buses
* (customChips/{busLogic,busNets,busKernel} + PinManager) evaluates a discrete
* logic-gate network exactly: switches drive nets, gates subscribe to their
* input nets and drive their output, and busKernel.settle() ripples the whole
* combinational network to its fixed point. Builds up simple -> complex, ending
* with the exact 4-bit adder/subtractor that the SPICE B-source path fails to
* light live (00-problem-analysis.md).
*
* This is the D-001 go/no-go gate: if the kernel can ripple a carry through a
* deep gate chain, the whole "gates on the digital engine" approach is sound.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { PinManager } from '../simulation/PinManager';
import { setBusDrive, resetBusNets } from '../simulation/customChips/busNets';
import { Strength, type Drive } from '../simulation/customChips/busLogic';
const strong = (value: 0 | 1): Drive => ({ value, strength: Strength.STRONG });
// Boolean primitives (match parts/LogicGateParts.ts semantics; XOR = parity).
const AND = (b: boolean[]) => b.every(Boolean);
const OR = (b: boolean[]) => b.some(Boolean);
const NAND = (b: boolean[]) => !AND(b);
const NOR = (b: boolean[]) => !OR(b);
const XOR = (b: boolean[]) => b.filter(Boolean).length % 2 === 1;
const XNOR = (b: boolean[]) => !XOR(b);
const NOT = (b: boolean[]) => !b[0];
/**
* A digital network on the settle kernel. Nets are integer keys (the same keys
* PinManager + busNets use for chip-to-chip nets). A switch is a STRONG driver;
* a gate subscribes to its input nets, recomputes on any change, and drives its
* output STRONG. Reading a net returns its resolved level.
*/
class Network {
readonly pm = new PinManager();
private nextKey = 1;
net(): number {
return this.nextKey++;
}
/** Drive a net from an input switch (STRONG). */
setSwitch(net: number, value: 0 | 1, id = `sw${net}`): void {
setBusDrive(this.pm, net, `${id}::o`, strong(value));
}
/** Read a net's resolved logic level (what an LED on it would show). */
read(net: number): 0 | 1 {
return this.pm.getPinState(net) ? 1 : 0;
}
/** A combinational gate: inputs[] -> output, recomputed event-driven. */
gate(id: string, inputs: number[], output: number, fn: (b: boolean[]) => boolean): void {
const state = inputs.map((n) => this.pm.getPinState(n));
const update = () => setBusDrive(this.pm, output, `${id}::Y`, strong(fn(state) ? 1 : 0));
inputs.forEach((n, i) =>
this.pm.onPinChange(n, (_p: number, s: boolean) => {
state[i] = s;
update();
}),
);
update(); // drive-on-mount so the network has a defined initial steady state
}
/** One full adder: returns {sum, cout} nets. */
fullAdder(tag: string, a: number, b: number, cin: number): { sum: number; cout: number } {
const axb = this.net();
const sum = this.net();
const ab = this.net();
const cab = this.net();
const cout = this.net();
this.gate(`${tag}_axb`, [a, b], axb, XOR);
this.gate(`${tag}_sum`, [axb, cin], sum, XOR);
this.gate(`${tag}_ab`, [a, b], ab, AND);
this.gate(`${tag}_cab`, [cin, axb], cab, AND);
this.gate(`${tag}_cout`, [ab, cab], cout, OR);
return { sum, cout };
}
}
beforeEach(() => resetBusNets());
describe('digital-gate-engine Phase 0 — single gates settle on the kernel', () => {
const cases: Array<[string, (b: boolean[]) => boolean, Array<[number, number, number]>]> = [
['AND', AND, [[0, 0, 0], [0, 1, 0], [1, 0, 0], [1, 1, 1]]],
['OR', OR, [[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 1]]],
['NAND', NAND, [[0, 0, 1], [0, 1, 1], [1, 0, 1], [1, 1, 0]]],
['NOR', NOR, [[0, 0, 1], [0, 1, 0], [1, 0, 0], [1, 1, 0]]],
['XOR', XOR, [[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0]]],
['XNOR', XNOR, [[0, 0, 1], [0, 1, 0], [1, 0, 0], [1, 1, 1]]],
];
it.each(cases)('%s truth table', (_name, fn, table) => {
for (const [a, b, y] of table) {
const net = new Network();
const A = net.net(), B = net.net(), Y = net.net();
net.gate('g', [A, B], Y, fn);
net.setSwitch(A, a as 0 | 1);
net.setSwitch(B, b as 0 | 1);
expect(net.read(Y), `${_name}(${a},${b})`).toBe(y);
resetBusNets();
}
});
it('NOT inverter (incl. the all-zero-input high output)', () => {
for (const [a, y] of [[0, 1], [1, 0]] as Array<[0 | 1, 0 | 1]>) {
const net = new Network();
const A = net.net(), Y = net.net();
net.gate('inv', [A], Y, NOT);
// Read BEFORE driving: NOT(0)=1 must come from the drive-on-mount.
expect(net.read(Y), `NOT(${a}) initial`).toBe(1);
net.setSwitch(A, a);
expect(net.read(Y), `NOT(${a})`).toBe(y);
resetBusNets();
}
});
});
describe('digital-gate-engine Phase 0 — combinational blocks', () => {
it('half adder: S = A XOR B, C = A AND B', () => {
for (const [a, b] of [[0, 0], [0, 1], [1, 0], [1, 1]] as Array<[0 | 1, 0 | 1]>) {
const net = new Network();
const A = net.net(), B = net.net(), S = net.net(), C = net.net();
net.gate('s', [A, B], S, XOR);
net.gate('c', [A, B], C, AND);
net.setSwitch(A, a);
net.setSwitch(B, b);
expect([net.read(S), net.read(C)], `HA(${a},${b})`).toEqual([a ^ b, a & b]);
resetBusNets();
}
});
it('full adder: all 8 input combinations', () => {
for (let v = 0; v < 8; v++) {
const a = (v & 1) as 0 | 1, b = ((v >> 1) & 1) as 0 | 1, cin = ((v >> 2) & 1) as 0 | 1;
const net = new Network();
const A = net.net(), B = net.net(), CIN = net.net();
const { sum, cout } = net.fullAdder('fa', A, B, CIN);
net.setSwitch(A, a);
net.setSwitch(B, b);
net.setSwitch(CIN, cin);
const total = a + b + cin;
expect([net.read(sum), net.read(cout)], `FA(${a},${b},${cin})`).toEqual([total & 1, total >> 1]);
resetBusNets();
}
});
});
describe('digital-gate-engine Phase 0 — 4-bit ripple adder/subtractor (the failing example)', () => {
// Builds the exact topology of /example/digital-adder-subtractor-4bit:
// each B bit XOR M, M -> FA0 carry-in, ripple chain; result = sum bits + carry.
const build = (N = 4) => {
const net = new Network();
const A = Array.from({ length: N }, () => net.net());
const B = Array.from({ length: N }, () => net.net());
const M = net.net();
let carry = M; // M feeds FA0 carry-in (two's-complement subtract)
const S: number[] = [];
for (let i = 0; i < N; i++) {
const bxm = net.net();
net.gate(`bxm${i}`, [B[i], M], bxm, XOR); // B_i XOR M
const { sum, cout } = net.fullAdder(`fa${i}`, A[i], bxm, carry);
S.push(sum);
carry = cout;
}
const apply = (a: number, b: number, m: 0 | 1) => {
net.setSwitch(M, m);
for (let i = 0; i < N; i++) {
net.setSwitch(A[i], ((a >> i) & 1) as 0 | 1);
net.setSwitch(B[i], ((b >> i) & 1) as 0 | 1);
}
};
const result = () => S.reduce((acc, s, i) => acc + (net.read(s) << i), 0);
const carryOut = () => net.read(carry);
return { apply, result, carryOut };
};
const vectors: Array<{ a: number; b: number; m: 0 | 1; sum: number; cout: 0 | 1; label: string }> = [
{ a: 3, b: 2, m: 0, sum: 5, cout: 0, label: 'ADD 3+2' },
{ a: 7, b: 6, m: 0, sum: 13, cout: 0, label: 'ADD 7+6' },
{ a: 15, b: 1, m: 0, sum: 0, cout: 1, label: 'ADD 15+1 (carry)' },
{ a: 9, b: 4, m: 0, sum: 13, cout: 0, label: 'ADD 9+4' },
{ a: 5, b: 2, m: 1, sum: 3, cout: 1, label: 'SUB 5-2' },
{ a: 9, b: 9, m: 1, sum: 0, cout: 1, label: 'SUB 9-9' },
{ a: 2, b: 5, m: 1, sum: 13, cout: 0, label: 'SUB 2-5 (borrow, 1101=-3)' },
];
it.each(vectors)('$label -> $sum (carry $cout)', ({ a, b, m, sum, cout }) => {
const adder = build(4);
adder.apply(a, b, m);
expect(adder.result()).toBe(sum);
expect(adder.carryOut()).toBe(cout);
});
it('exhaustive ADD: every A,B in 0..15 gives (A+B) mod 16 + carry', () => {
for (let a = 0; a < 16; a++) {
for (let b = 0; b < 16; b++) {
const adder = build(4);
adder.apply(a, b, 0);
const total = a + b;
expect(adder.result(), `ADD ${a}+${b} sum`).toBe(total & 15);
expect(adder.carryOut(), `ADD ${a}+${b} carry`).toBe(((total >> 4) & 1) as 0 | 1);
resetBusNets();
}
}
});
});
describe('digital-gate-engine Phase 0 — more example topologies (fan-out, select, wide, deep)', () => {
it('2-to-1 mux: Y = S ? B : A (all 8 inputs)', () => {
for (let v = 0; v < 8; v++) {
const s = (v & 1) as 0 | 1, a = ((v >> 1) & 1) as 0 | 1, b = ((v >> 2) & 1) as 0 | 1;
const net = new Network();
const S = net.net(), A = net.net(), B = net.net();
const nS = net.net(), t0 = net.net(), t1 = net.net(), Y = net.net();
net.gate('ns', [S], nS, NOT);
net.gate('t0', [nS, A], t0, AND);
net.gate('t1', [S, B], t1, AND);
net.gate('y', [t0, t1], Y, OR);
net.setSwitch(S, s); net.setSwitch(A, a); net.setSwitch(B, b);
expect(net.read(Y), `MUX s=${s} a=${a} b=${b}`).toBe(s ? b : a);
resetBusNets();
}
});
it('2-to-4 decoder: one-hot output (fan-out from 2 inputs)', () => {
for (let v = 0; v < 4; v++) {
const s0 = (v & 1) as 0 | 1, s1 = ((v >> 1) & 1) as 0 | 1;
const net = new Network();
const S0 = net.net(), S1 = net.net(), nS0 = net.net(), nS1 = net.net();
const D = [net.net(), net.net(), net.net(), net.net()];
net.gate('n0', [S0], nS0, NOT);
net.gate('n1', [S1], nS1, NOT);
net.gate('d0', [nS1, nS0], D[0], AND);
net.gate('d1', [nS1, S0], D[1], AND);
net.gate('d2', [S1, nS0], D[2], AND);
net.gate('d3', [S1, S0], D[3], AND);
net.setSwitch(S0, s0); net.setSwitch(S1, s1);
expect(D.map((d) => net.read(d)), `DECODE ${v}`).toEqual([0, 1, 2, 3].map((i) => (i === v ? 1 : 0)));
resetBusNets();
}
});
it('4-bit equality comparator: EQ = AND of (A_i XNOR B_i) — wide AND', () => {
const samples: Array<[number, number]> = [[0, 0], [5, 5], [15, 15], [5, 7], [9, 1], [15, 14]];
for (const [a, b] of samples) {
const net = new Network();
const e: number[] = [];
for (let i = 0; i < 4; i++) {
const Ai = net.net(), Bi = net.net(), Ei = net.net();
net.gate(`xnor${i}`, [Ai, Bi], Ei, XNOR);
net.setSwitch(Ai, ((a >> i) & 1) as 0 | 1);
net.setSwitch(Bi, ((b >> i) & 1) as 0 | 1);
e.push(Ei);
}
const EQ = net.net();
net.gate('eq', e, EQ, AND); // 4-input AND
expect(net.read(EQ), `EQ ${a}==${b}`).toBe(a === b ? 1 : 0);
resetBusNets();
}
});
it('4-bit parity: cascaded XOR chain (depth) — odd-1s detector', () => {
for (let v = 0; v < 16; v++) {
const net = new Network();
const bits = [net.net(), net.net(), net.net(), net.net()];
const p01 = net.net(), p012 = net.net(), p0123 = net.net();
net.gate('p01', [bits[0], bits[1]], p01, XOR);
net.gate('p012', [p01, bits[2]], p012, XOR);
net.gate('p0123', [p012, bits[3]], p0123, XOR);
bits.forEach((bnet, i) => net.setSwitch(bnet, ((v >> i) & 1) as 0 | 1));
const ones = [0, 1, 2, 3].reduce((n, i) => n + ((v >> i) & 1), 0);
expect(net.read(p0123), `PARITY ${v}`).toBe((ones & 1) as 0 | 1);
resetBusNets();
}
});
it('2x2 binary multiplier: partial products + half adders (mixed arithmetic)', () => {
for (let a = 0; a < 4; a++) {
for (let b = 0; b < 4; b++) {
const net = new Network();
const A0 = net.net(), A1 = net.net(), B0 = net.net(), B1 = net.net();
const a0b0 = net.net(), a1b0 = net.net(), a0b1 = net.net(), a1b1 = net.net();
net.gate('a0b0', [A0, B0], a0b0, AND);
net.gate('a1b0', [A1, B0], a1b0, AND);
net.gate('a0b1', [A0, B1], a0b1, AND);
net.gate('a1b1', [A1, B1], a1b1, AND);
const P0 = a0b0;
const P1 = net.net(), c1 = net.net();
net.gate('p1', [a1b0, a0b1], P1, XOR);
net.gate('c1', [a1b0, a0b1], c1, AND);
const P2 = net.net(), c2 = net.net();
net.gate('p2', [a1b1, c1], P2, XOR);
net.gate('c2', [a1b1, c1], c2, AND);
const P3 = c2;
net.setSwitch(A0, (a & 1) as 0 | 1); net.setSwitch(A1, ((a >> 1) & 1) as 0 | 1);
net.setSwitch(B0, (b & 1) as 0 | 1); net.setSwitch(B1, ((b >> 1) & 1) as 0 | 1);
const product = net.read(P0) + (net.read(P1) << 1) + (net.read(P2) << 2) + (net.read(P3) << 3);
expect(product, `MUL ${a}*${b}`).toBe(a * b);
resetBusNets();
}
}
});
});