Files
logic-gates/js/gates.js
Jose Luis a4292b42cf fix: iterative evaluation for sequential circuits + debug logs
Replace single-pass recursive evaluation with iterative fixed-point
evaluation that runs multiple passes until all gate values stabilize.
Crucially, gate values are NO LONGER reset to 0 before evaluation,
which preserves latch/flip-flop memory state.

Add console logs for toggle, wire, and evaluation stability debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 03:59:26 +01:00

162 lines
5.6 KiB
JavaScript

// Gate evaluation and port geometry
import { GATE_W, GATE_H, COMP_W, PORT_R, gateInputCount as baseGateInputCount, gateOutputCount as baseGateOutputCount } from './constants.js';
import { state } from './state.js';
import { recordSample, setEvaluateAll } from './waveform.js';
import { evaluateComponent } from './components.js';
// Wrappers that handle component types
export function gateInputCount(type) {
if (type.startsWith('COMPONENT:')) {
const componentId = type.substring(10);
const component = state.customComponents?.[componentId];
return component ? component.inputCount : 0;
}
return baseGateInputCount(type);
}
export function gateOutputCount(type) {
if (type.startsWith('COMPONENT:')) {
const componentId = type.substring(10);
const component = state.customComponents?.[componentId];
return component ? component.outputCount : 0;
}
return baseGateOutputCount(type);
}
export function getComponentWidth(gate) {
if (gate.type.startsWith('COMPONENT:')) {
const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1);
return Math.max(120, (count + 1) * 25);
}
return GATE_W;
}
export function getComponentHeight(gate) {
if (gate.type.startsWith('COMPONENT:')) {
const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1);
return Math.max(60, (count + 1) * 25);
}
return GATE_H;
}
export function getInputPorts(gate) {
const count = gateInputCount(gate.type);
const ports = [];
const isComponent = gate.type.startsWith('COMPONENT:');
const gateHeight = isComponent ? getComponentHeight(gate) : GATE_H;
for (let i = 0; i < count; i++) {
const spacing = gateHeight / (count + 1);
ports.push({ x: gate.x, y: gate.y + spacing * (i + 1), index: i, type: 'input' });
}
return ports;
}
export function getOutputPorts(gate) {
const count = gateOutputCount(gate.type);
const ports = [];
const isComponent = gate.type.startsWith('COMPONENT:');
const gateWidth = isComponent ? getComponentWidth(gate) : GATE_W;
const gateHeight = isComponent ? getComponentHeight(gate) : GATE_H;
for (let i = 0; i < count; i++) {
const spacing = gateHeight / (count + 1);
ports.push({ x: gate.x + gateWidth, y: gate.y + spacing * (i + 1), index: i, type: 'output' });
}
return ports;
}
/**
* Compute the output of a single gate given its current input values.
* Does NOT recurse — just reads source gate .value directly.
*/
function computeGate(gate) {
if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value;
const inputCount = gateInputCount(gate.type);
const inputs = [];
for (let i = 0; i < inputCount; i++) {
const conn = state.connections.find(c => c.to === gate.id && c.toPort === i);
if (conn) {
const srcGate = state.gates.find(g => g.id === conn.from);
inputs.push(srcGate ? (srcGate.value || 0) : 0);
} else {
inputs.push(0);
}
}
if (gate.type.startsWith('COMPONENT:')) {
const outputs = evaluateComponent(gate, inputs);
return outputs[0] || 0;
}
switch (gate.type) {
case 'AND': return (inputs[0] && inputs[1]) ? 1 : 0;
case 'OR': return (inputs[0] || inputs[1]) ? 1 : 0;
case 'NOT': return inputs[0] ? 0 : 1;
case 'NAND': return (inputs[0] && inputs[1]) ? 0 : 1;
case 'NOR': return (inputs[0] || inputs[1]) ? 0 : 1;
case 'XOR': return (inputs[0] !== inputs[1]) ? 1 : 0;
case 'OUTPUT': return inputs[0] || 0;
default: return 0;
}
}
/**
* Iterative fixed-point evaluation.
* Runs multiple passes over all gates until no values change (stable)
* or a max iteration limit is reached. Does NOT reset gate values,
* preserving latch/flip-flop state across evaluations.
*/
const MAX_ITERATIONS = 20;
export function evaluateAll(recordWave = false) {
for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
let changed = false;
for (const gate of state.gates) {
if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue;
const newVal = computeGate(gate);
if (newVal !== gate.value) {
gate.value = newVal;
changed = true;
}
}
if (!changed) {
console.log(`[eval] stable after ${iter + 1} iteration(s)`);
break;
}
if (iter === MAX_ITERATIONS - 1) {
console.warn(`[eval] did not stabilize after ${MAX_ITERATIONS} iterations (oscillation?)`);
}
}
if (recordWave && state.recording && state.waveformVisible) recordSample();
}
// Keep legacy export name for components.js internal use
export function evaluate(gate) {
return computeGate(gate);
}
// Register evaluateAll in waveform to break circular dependency
setEvaluateAll(evaluateAll);
export function findGateAt(x, y) {
return state.gates.find(g => {
const w = g.type.startsWith('COMPONENT:') ? getComponentWidth(g) : GATE_W;
const h = g.type.startsWith('COMPONENT:') ? getComponentHeight(g) : GATE_H;
return x >= g.x && x <= g.x + w && y >= g.y && y <= g.y + h;
});
}
export function findPortAt(x, y) {
for (const gate of state.gates) {
for (const p of getInputPorts(gate)) {
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'input' };
}
for (const p of getOutputPorts(gate)) {
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'output' };
}
}
return null;
}