301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
/**
|
|
* digitalGateEngine — evaluate a board-less DIGITAL circuit (logic gates +
|
|
* switches + LEDs + power rails) on the event-driven settle kernel instead of
|
|
* ngspice B-sources. Phase 1 of project/digital-gate-engine/.
|
|
*
|
|
* It reuses the multichip-bus substrate (customChips/{busLogic,busNets,
|
|
* busKernel} + PinManager): every wire-connected set of pins becomes one bus
|
|
* net key, each primitive contributes a driver (or, for gates, an event-driven
|
|
* compute), and busKernel.settle() ripples the network to its fixed point. The
|
|
* same kernel that boots a Z80 over a chip bus evaluates the gate network — so a
|
|
* 4-bit ripple adder settles exactly, which the cascaded-B-source SPICE model
|
|
* does not (00-problem-analysis.md).
|
|
*
|
|
* Digital abstraction of the analog scaffolding the examples use:
|
|
* - signal-generator SIG = STRONG 1 (the 5 V rail); its GND pin = node 0.
|
|
* - a resistor with one end on GND = PULL 0 on the other net (pull-down).
|
|
* - a resistor with one end on rail = PULL 1 on the other net (pull-up).
|
|
* - a resistor between two signal nets = pass-through (the nets merge).
|
|
* - a slide-switch closed = pass its rail-side level to its other pin (STRONG);
|
|
* open = Hi-Z (the pull-down then wins -> 0).
|
|
* - a gate computes its boolean and drives Y STRONG.
|
|
* - an LED is a pure sink: it reads its anode net (lit iff the net is 1).
|
|
*
|
|
* `buildDigitalNetwork` returns a controller: drive switches, read LED/net
|
|
* levels. It does not touch the DOM or the store — the app layer (Phase 2)
|
|
* wires those in.
|
|
*/
|
|
import { PinManager } from '../PinManager';
|
|
import { setBusDrive } from '../customChips/busNets';
|
|
import { Strength, type Drive } from '../customChips/busLogic';
|
|
|
|
const STRONG = (v: 0 | 1): Drive => ({ value: v, strength: Strength.STRONG });
|
|
const PULL = (v: 0 | 1): Drive => ({ value: v, strength: Strength.PULL });
|
|
|
|
export interface DigitalComponent {
|
|
id: string;
|
|
/** Raw example type (`velxio-logic-gate-and`, `wokwi-slide-switch`, …). */
|
|
type?: string;
|
|
/** Store-normalised id (`logic-gate-and`, `slide-switch`, …). */
|
|
metadataId?: string;
|
|
properties?: Record<string, unknown>;
|
|
}
|
|
export interface DigitalWire {
|
|
start: { componentId: string; pinName: string };
|
|
end: { componentId: string; pinName: string };
|
|
}
|
|
|
|
/**
|
|
* Canonical kind for a component, tolerant of both shapes: the raw example data
|
|
* carries `type: 'velxio-logic-gate-and'` / `'wokwi-led'`, the loaded store
|
|
* carries `metadataId: 'logic-gate-and'` / `'led'`. Strip the vendor prefixes so
|
|
* both resolve to the same kind.
|
|
*/
|
|
function kindOf(c: DigitalComponent): string {
|
|
const raw = String(c.metadataId ?? c.type ?? '');
|
|
return raw.replace(/^velxio-/, '').replace(/^wokwi-/, '');
|
|
}
|
|
|
|
// Boolean primitives (match parts/LogicGateParts.ts; XOR = parity).
|
|
const OPS: Record<string, (b: boolean[]) => boolean> = {
|
|
and: (b) => b.every(Boolean),
|
|
or: (b) => b.some(Boolean),
|
|
nand: (b) => !b.every(Boolean),
|
|
nor: (b) => !b.some(Boolean),
|
|
xor: (b) => b.filter(Boolean).length % 2 === 1,
|
|
xnor: (b) => b.filter(Boolean).length % 2 === 0,
|
|
not: (b) => !b[0],
|
|
buffer: (b) => !!b[0],
|
|
};
|
|
|
|
/** Parse a normalised gate kind `logic-gate-<base>(-<n>)?` into pins + fn. */
|
|
function parseGate(kind: string): { inputs: string[]; fn: (b: boolean[]) => boolean } | null {
|
|
const m = /^logic-gate-([a-z]+)(?:-(\d))?$/.exec(kind);
|
|
if (!m) return null;
|
|
const base = m[1];
|
|
const fn = OPS[base];
|
|
if (!fn) return null;
|
|
if (base === 'not' || base === 'buffer') return { inputs: ['A'], fn };
|
|
const n = m[2] ? Number(m[2]) : 2;
|
|
const inputs = ['A', 'B', 'C', 'D'].slice(0, n);
|
|
return { inputs, fn };
|
|
}
|
|
|
|
const isGate = (t: string) => t.startsWith('logic-gate-');
|
|
const isSwitch = (t: string) => t === 'slide-switch';
|
|
const isLed = (t: string) => t === 'led';
|
|
const isResistor = (t: string) => t === 'resistor';
|
|
const isPower = (t: string) => t === 'signal-generator';
|
|
|
|
/** Components this engine understands. Anything else => analog => bail. */
|
|
function isDigitalPrimitive(t: string): boolean {
|
|
return isGate(t) || isSwitch(t) || isLed(t) || isResistor(t) || isPower(t);
|
|
}
|
|
|
|
/** Opt-in flag, mirrors chipBusEnabled / mixedmode. Default OFF until verified. */
|
|
export function digitalGatesEnabled(): boolean {
|
|
try {
|
|
if (typeof window !== 'undefined' && window.location) {
|
|
const q = new URLSearchParams(window.location.search).get('digitalgates');
|
|
if (q === 'on' || q === '1' || q === 'true') return true;
|
|
if (q === 'off' || q === '0' || q === 'false') return false;
|
|
}
|
|
if (typeof localStorage !== 'undefined') {
|
|
const v = localStorage.getItem('velxio.digitalgates');
|
|
if (v === 'on' || v === '1' || v === 'true') return true;
|
|
if (v === 'off' || v === '0' || v === 'false') return false;
|
|
}
|
|
} catch {
|
|
/* missing globals in tests / SecurityError — fall through */
|
|
}
|
|
// On by default: all-digital gate circuits are evaluated exactly + instantly by
|
|
// the event-driven engine (the ngspice B-source path could not light a 4-bit
|
|
// adder). Only pure all-digital-with-a-gate circuits take this path; mixed /
|
|
// analog circuits stay on ngspice. Override with ?digitalgates=off.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* True iff every component is a digital primitive AND at least one is a logic
|
|
* gate. The gate requirement keeps the engine from claiming degenerate analog
|
|
* circuits that happen to use only {source, resistor, LED} with no logic — those
|
|
* stay on ngspice.
|
|
*/
|
|
export function isAllDigital(components: DigitalComponent[]): boolean {
|
|
if (components.length === 0) return false;
|
|
if (!components.every((c) => isDigitalPrimitive(kindOf(c)))) return false;
|
|
return components.some((c) => isGate(kindOf(c)));
|
|
}
|
|
|
|
// Endpoint key. A printable separator (NOT a space — a lone space gets stored
|
|
// as a NUL byte by the edit tools, turning the source into a git-binary).
|
|
const epKey = (compId: string, pin: string) => `${compId}::${pin}`;
|
|
|
|
// ── Union-find over wire endpoints ──────────────────────────────────────────
|
|
class UnionFind {
|
|
private parent = new Map<string, string>();
|
|
find(x: string): string {
|
|
let r = this.parent.get(x);
|
|
if (r === undefined) {
|
|
this.parent.set(x, x);
|
|
return x;
|
|
}
|
|
while (r !== this.parent.get(r)) {
|
|
const gp = this.parent.get(r)!;
|
|
this.parent.set(r, this.parent.get(gp)!);
|
|
r = gp;
|
|
}
|
|
return r;
|
|
}
|
|
union(a: string, b: string): void {
|
|
const ra = this.find(a), rb = this.find(b);
|
|
if (ra !== rb) this.parent.set(ra, rb);
|
|
}
|
|
}
|
|
|
|
export interface DigitalNetwork {
|
|
/** True if every component was a digital primitive (else nothing was built). */
|
|
ok: boolean;
|
|
pinManager: PinManager;
|
|
/** Resolve a component pin to its bus-net key (or undefined). */
|
|
netOf(componentId: string, pin: string): number | undefined;
|
|
/** Read a net's resolved logic level. */
|
|
readNet(net: number): 0 | 1;
|
|
/** Read an LED's lit state (its anode net level). */
|
|
readLed(ledId: string): 0 | 1;
|
|
/** Set a slide-switch open/closed and re-settle. */
|
|
setSwitch(switchId: string, value: 0 | 1): void;
|
|
/** All LED ids in the network. */
|
|
ledIds: string[];
|
|
}
|
|
|
|
/**
|
|
* Build the digital network. Returns `{ ok:false }` (and drives nothing) if any
|
|
* component is not a digital primitive — that circuit belongs to ngspice.
|
|
*/
|
|
export function buildDigitalNetwork(
|
|
components: DigitalComponent[],
|
|
wires: DigitalWire[],
|
|
pinManager?: PinManager,
|
|
): DigitalNetwork {
|
|
const pm = pinManager ?? new PinManager();
|
|
const noop: DigitalNetwork = {
|
|
ok: false, pinManager: pm,
|
|
netOf: () => undefined, readNet: () => 0, readLed: () => 0, setSwitch: () => {}, ledIds: [],
|
|
};
|
|
if (components.some((c) => !isDigitalPrimitive(kindOf(c)))) return noop;
|
|
|
|
const byId = new Map(components.map((c) => [c.id, c]));
|
|
const uf = new UnionFind();
|
|
for (const w of wires) uf.union(epKey(w.start.componentId, w.start.pinName), epKey(w.end.componentId, w.end.pinName));
|
|
|
|
const pinNet = (compId: string, pin: string) => uf.find(epKey(compId, pin));
|
|
|
|
// Identify the GND and rail roots from the signal-generator(s).
|
|
const findRailGnd = (gnd: Set<string>, rail: Set<string>) => {
|
|
gnd.clear(); rail.clear();
|
|
for (const c of components) {
|
|
if (isPower(kindOf(c))) {
|
|
gnd.add(pinNet(c.id, 'GND'));
|
|
rail.add(pinNet(c.id, 'SIG'));
|
|
}
|
|
}
|
|
};
|
|
const gndRoots = new Set<string>();
|
|
const railRoots = new Set<string>();
|
|
findRailGnd(gndRoots, railRoots);
|
|
const isGnd = (root: string) => gndRoots.has(root);
|
|
const isRail = (root: string) => railRoots.has(root);
|
|
|
|
// Pass-through resistor merge (neither end on rail/gnd), then recompute roots.
|
|
for (const c of components) {
|
|
if (!isResistor(kindOf(c))) continue;
|
|
const r1 = pinNet(c.id, '1'), r2 = pinNet(c.id, '2');
|
|
const special = (r: string) => isGnd(r) || isRail(r);
|
|
if (!special(r1) && !special(r2)) uf.union(epKey(c.id, '1'), epKey(c.id, '2'));
|
|
}
|
|
findRailGnd(gndRoots, railRoots);
|
|
|
|
// Assign an integer key per net root.
|
|
const keyOf = new Map<string, number>();
|
|
let nextKey = 1;
|
|
const netKey = (compId: string, pin: string): number => {
|
|
const root = pinNet(compId, pin);
|
|
let k = keyOf.get(root);
|
|
if (k === undefined) { k = nextKey++; keyOf.set(root, k); }
|
|
return k;
|
|
};
|
|
const netOf = (compId: string, pin: string): number | undefined => {
|
|
if (!byId.has(compId)) return undefined;
|
|
return netKey(compId, pin);
|
|
};
|
|
|
|
// ── Static drivers: rail, gnd, pull resistors ──────────────────────────────
|
|
for (const c of components) {
|
|
if (isPower(kindOf(c))) {
|
|
setBusDrive(pm, netKey(c.id, 'SIG'), `${c.id}::SIG`, STRONG(1)); // 5 V rail
|
|
setBusDrive(pm, netKey(c.id, 'GND'), `${c.id}::GND`, STRONG(0)); // node 0
|
|
}
|
|
}
|
|
for (const c of components) {
|
|
if (!isResistor(kindOf(c))) continue;
|
|
const r1 = pinNet(c.id, '1'), r2 = pinNet(c.id, '2');
|
|
if (isGnd(r1) && !isGnd(r2)) setBusDrive(pm, netKey(c.id, '2'), `${c.id}::pd`, PULL(0));
|
|
else if (isGnd(r2) && !isGnd(r1)) setBusDrive(pm, netKey(c.id, '1'), `${c.id}::pd`, PULL(0));
|
|
else if (isRail(r1) && !isRail(r2)) setBusDrive(pm, netKey(c.id, '2'), `${c.id}::pu`, PULL(1));
|
|
else if (isRail(r2) && !isRail(r1)) setBusDrive(pm, netKey(c.id, '1'), `${c.id}::pu`, PULL(1));
|
|
// else: pass-through (already merged) — contributes no driver.
|
|
}
|
|
|
|
// ── Switches: closed passes the rail-side level to the other pin ───────────
|
|
const switchState = new Map<string, 0 | 1>();
|
|
const driveSwitch = (c: DigitalComponent) => {
|
|
const closed = switchState.get(c.id) ?? (Number(c.properties?.value) === 1 ? 1 : 0);
|
|
const n1 = netKey(c.id, '1'), n2 = netKey(c.id, '2');
|
|
const root1 = pinNet(c.id, '1');
|
|
// switchInput() wires pin '1' to the rail, pin '2' to the gate input. Drive
|
|
// the gate side with the source side's level when closed, else release.
|
|
const [src, dst] = isRail(root1) || !isGnd(pinNet(c.id, '2')) ? [n1, n2] : [n2, n1];
|
|
if (closed) {
|
|
const srcLevel = pm.getPinState(src) ? 1 : 0;
|
|
setBusDrive(pm, dst, `${c.id}::pass`, STRONG(srcLevel as 0 | 1));
|
|
} else {
|
|
setBusDrive(pm, dst, `${c.id}::pass`, { value: 0, strength: Strength.HIGHZ });
|
|
}
|
|
};
|
|
for (const c of components) if (isSwitch(kindOf(c))) driveSwitch(c);
|
|
|
|
// ── Gates: subscribe inputs, compute, drive Y (event-driven) ───────────────
|
|
for (const c of components) {
|
|
if (!isGate(kindOf(c))) continue;
|
|
const spec = parseGate(kindOf(c));
|
|
if (!spec) continue;
|
|
const inNets = spec.inputs.map((p) => netKey(c.id, p));
|
|
const outNet = netKey(c.id, 'Y');
|
|
const st = inNets.map((n) => pm.getPinState(n));
|
|
const update = () => setBusDrive(pm, outNet, `${c.id}::Y`, STRONG(spec.fn(st) ? 1 : 0));
|
|
inNets.forEach((n, i) => pm.onPinChange(n, (_p, s) => { st[i] = s; update(); }));
|
|
update();
|
|
}
|
|
|
|
// Re-drive switches now that rail levels have settled (a switch built before
|
|
// its rail driver landed would have passed a stale 0).
|
|
for (const c of components) if (isSwitch(kindOf(c))) driveSwitch(c);
|
|
|
|
const ledIds = components.filter((c) => isLed(kindOf(c))).map((c) => c.id);
|
|
|
|
return {
|
|
ok: true,
|
|
pinManager: pm,
|
|
netOf,
|
|
readNet: (net) => (pm.getPinState(net) ? 1 : 0),
|
|
readLed: (ledId) => (pm.getPinState(netKey(ledId, 'A')) ? 1 : 0),
|
|
setSwitch: (switchId, value) => {
|
|
switchState.set(switchId, value);
|
|
const c = byId.get(switchId);
|
|
if (c) driveSwitch(c);
|
|
},
|
|
ledIds,
|
|
};
|
|
}
|