309 lines
12 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
});
|
|
});
|