From 4f4ee0bf60f88bad41d4796400b8b78cc48eabee Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 5 Jun 2026 20:29:32 -0300 Subject: [PATCH] feat(digital-gate-engine): sweep all 38 examples + default the flag on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 (brought forward before the mixed-mode boundary). digitalgate-sweep proves the engine handles 38/38 gallery digital examples: every one builds, resolves every LED, and never oscillates. Tightened isAllDigital to also require at least one logic gate, so a degenerate analog {source, resistor, LED} circuit stays on ngspice rather than being claimed by the digital path. Flipped digitalGatesEnabled() default to ON (override with ?digitalgates=off). Full frontend suite 2120 pass / 5 fail — the 5 are the same pre-existing unrelated failures (ngspice node-path, attiny85 arduino-cli, component-to-spice catalog); the default flip adds no new breakage and examples-digital + circuit-simulation-service stay green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/__tests__/digitalgate-sweep.test.ts | 73 +++++++++++++++++++ .../simulation/digital/digitalGateEngine.ts | 17 ++++- 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 frontend/src/__tests__/digitalgate-sweep.test.ts diff --git a/frontend/src/__tests__/digitalgate-sweep.test.ts b/frontend/src/__tests__/digitalgate-sweep.test.ts new file mode 100644 index 00000000..0d435a8a --- /dev/null +++ b/frontend/src/__tests__/digitalgate-sweep.test.ts @@ -0,0 +1,73 @@ +/** + * digital-gate-engine Phase 3 (sweep) — every gallery digital example is handled + * by the engine without crashing, and the all-digital ones resolve cleanly. + * + * For each of the 38 `examples-digital.ts` circuits: build the network from the + * real data, and if it is all-digital, drive its switches through a few vectors + * and confirm every LED resolves (no throw, no settle blow-up). Examples that + * include a non-primitive (e.g. a 7-segment display) legitimately bail to + * ngspice — those are reported, not failed. This is the gate for flipping the + * `?digitalgates` default on. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { resetBusNets } from '../simulation/customChips/busNets'; +import { buildDigitalNetwork, isAllDigital, type DigitalComponent, type DigitalWire } from '../simulation/digital/digitalGateEngine'; +import { digitalExamples } from '../data/examples-digital'; + +type Ex = { id: string; components: DigitalComponent[]; wires: DigitalWire[] }; +const all = digitalExamples as unknown as Ex[]; + +beforeEach(() => resetBusNets()); + +describe('digital-gate-engine sweep — all gallery digital examples', () => { + it('there are at least 35 digital examples to sweep', () => { + expect(all.length).toBeGreaterThanOrEqual(35); + }); + + it('every example is either all-digital-and-handled or cleanly bails to ngspice', () => { + const handled: string[] = []; + const bailed: string[] = []; + for (const ex of all) { + const allDigital = isAllDigital(ex.components); + const net = buildDigitalNetwork(ex.components, ex.wires); + if (allDigital) { + expect(net.ok, `${ex.id} is all-digital so should build`).toBe(true); + handled.push(ex.id); + } else { + expect(net.ok, `${ex.id} has a non-primitive so must bail`).toBe(false); + bailed.push(ex.id); + } + resetBusNets(); + } + // The vast majority are pure gate circuits; only a couple (e.g. 7-seg) bail. + expect(handled.length, `handled: ${handled.length}, bailed: ${bailed.join(', ')}`).toBeGreaterThanOrEqual(33); + // eslint-disable-next-line no-console + console.log(`[sweep] engine handles ${handled.length}/${all.length}; bails (ngspice): ${bailed.join(', ') || 'none'}`); + }); + + it('each handled example drives + reads every LED without throwing or oscillating', () => { + const warn = console.warn; + let oscillations = 0; + console.warn = (...a: unknown[]) => { if (String(a[0]).includes('did not converge')) oscillations++; }; + try { + for (const ex of all) { + if (!isAllDigital(ex.components)) continue; + const switchIds = ex.components.filter((c) => String(c.metadataId ?? c.type ?? '').includes('slide-switch')).map((c) => c.id); + // a few input vectors: all-low, all-high, alternating + for (const pattern of [() => 0 as const, () => 1 as const, (i: number) => (i % 2) as 0 | 1]) { + const net = buildDigitalNetwork(ex.components, ex.wires); + expect(net.ok).toBe(true); + switchIds.forEach((id, i) => net.setSwitch(id, pattern(i))); + for (const led of net.ledIds) { + const v = net.readLed(led); + expect(v === 0 || v === 1, `${ex.id} LED ${led} resolved`).toBe(true); + } + resetBusNets(); + } + } + } finally { + console.warn = warn; + } + expect(oscillations, 'no combinational-loop oscillation in any example').toBe(0); + }); +}); diff --git a/frontend/src/simulation/digital/digitalGateEngine.ts b/frontend/src/simulation/digital/digitalGateEngine.ts index fa238a78..1c3a31cf 100644 --- a/frontend/src/simulation/digital/digitalGateEngine.ts +++ b/frontend/src/simulation/digital/digitalGateEngine.ts @@ -108,12 +108,23 @@ export function digitalGatesEnabled(): boolean { } catch { /* missing globals in tests / SecurityError — fall through */ } - return false; + // 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 (so the engine can own it). */ +/** + * 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 { - return components.length > 0 && components.every((c) => isDigitalPrimitive(kindOf(c))); + 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