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>
This commit is contained in:
13
js/events.js
13
js/events.js
@@ -100,12 +100,18 @@ export function initEvents() {
|
|||||||
if (state.connecting) {
|
if (state.connecting) {
|
||||||
if (state.connecting.portType === 'output' && port.type === 'input') {
|
if (state.connecting.portType === 'output' && port.type === 'input') {
|
||||||
state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
||||||
state.connections.push({ from: state.connecting.gate.id, fromPort: state.connecting.portIndex, to: port.gate.id, toPort: port.index });
|
const conn = { from: state.connecting.gate.id, fromPort: state.connecting.portIndex, to: port.gate.id, toPort: port.index };
|
||||||
|
state.connections.push(conn);
|
||||||
|
console.log(`[wire] ${conn.from}:${conn.fromPort} → ${conn.to}:${conn.toPort}`);
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
||||||
} else if (state.connecting.portType === 'input' && port.type === 'output') {
|
} else if (state.connecting.portType === 'input' && port.type === 'output') {
|
||||||
state.connections = state.connections.filter(c => !(c.to === state.connecting.gate.id && c.toPort === state.connecting.portIndex));
|
state.connections = state.connections.filter(c => !(c.to === state.connecting.gate.id && c.toPort === state.connecting.portIndex));
|
||||||
state.connections.push({ from: port.gate.id, fromPort: port.index, to: state.connecting.gate.id, toPort: state.connecting.portIndex });
|
const conn = { from: port.gate.id, fromPort: port.index, to: state.connecting.gate.id, toPort: state.connecting.portIndex };
|
||||||
|
state.connections.push(conn);
|
||||||
|
console.log(`[wire] ${conn.from}:${conn.fromPort} → ${conn.to}:${conn.toPort}`);
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
||||||
}
|
}
|
||||||
state.connecting = null;
|
state.connecting = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -131,7 +137,10 @@ export function initEvents() {
|
|||||||
const gate = state.dragging;
|
const gate = state.dragging;
|
||||||
if (gate.type === 'INPUT' || gate.type === 'CLOCK') {
|
if (gate.type === 'INPUT' || gate.type === 'CLOCK') {
|
||||||
gate.value = gate.value ? 0 : 1;
|
gate.value = gate.value ? 0 : 1;
|
||||||
|
console.log(`[toggle] ${gate.type}#${gate.id} → ${gate.value}`);
|
||||||
evaluateAll(true); // record waveform on intentional toggle
|
evaluateAll(true); // record waveform on intentional toggle
|
||||||
|
// Log all gate values after evaluation
|
||||||
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.dragging = null;
|
state.dragging = null;
|
||||||
|
|||||||
71
js/gates.js
71
js/gates.js
@@ -66,9 +66,11 @@ export function getOutputPorts(gate) {
|
|||||||
return ports;
|
return ports;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evaluate(gate, visited = new Set()) {
|
/**
|
||||||
if (visited.has(gate.id)) return gate.value || 0;
|
* Compute the output of a single gate given its current input values.
|
||||||
visited.add(gate.id);
|
* Does NOT recurse — just reads source gate .value directly.
|
||||||
|
*/
|
||||||
|
function computeGate(gate) {
|
||||||
if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value;
|
if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value;
|
||||||
|
|
||||||
const inputCount = gateInputCount(gate.type);
|
const inputCount = gateInputCount(gate.type);
|
||||||
@@ -77,39 +79,64 @@ export function evaluate(gate, visited = new Set()) {
|
|||||||
const conn = state.connections.find(c => c.to === gate.id && c.toPort === i);
|
const conn = state.connections.find(c => c.to === gate.id && c.toPort === i);
|
||||||
if (conn) {
|
if (conn) {
|
||||||
const srcGate = state.gates.find(g => g.id === conn.from);
|
const srcGate = state.gates.find(g => g.id === conn.from);
|
||||||
inputs.push(srcGate ? evaluate(srcGate, visited) : 0);
|
inputs.push(srcGate ? (srcGate.value || 0) : 0);
|
||||||
} else {
|
} else {
|
||||||
inputs.push(0);
|
inputs.push(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = 0;
|
|
||||||
if (gate.type.startsWith('COMPONENT:')) {
|
if (gate.type.startsWith('COMPONENT:')) {
|
||||||
const outputs = evaluateComponent(gate, inputs);
|
const outputs = evaluateComponent(gate, inputs);
|
||||||
result = outputs[0] || 0;
|
return outputs[0] || 0;
|
||||||
} else {
|
}
|
||||||
switch (gate.type) {
|
|
||||||
case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break;
|
switch (gate.type) {
|
||||||
case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break;
|
case 'AND': return (inputs[0] && inputs[1]) ? 1 : 0;
|
||||||
case 'NOT': result = inputs[0] ? 0 : 1; break;
|
case 'OR': return (inputs[0] || inputs[1]) ? 1 : 0;
|
||||||
case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break;
|
case 'NOT': return inputs[0] ? 0 : 1;
|
||||||
case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break;
|
case 'NAND': return (inputs[0] && inputs[1]) ? 0 : 1;
|
||||||
case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break;
|
case 'NOR': return (inputs[0] || inputs[1]) ? 0 : 1;
|
||||||
case 'OUTPUT': result = inputs[0] || 0; break;
|
case 'XOR': return (inputs[0] !== inputs[1]) ? 1 : 0;
|
||||||
}
|
case 'OUTPUT': return inputs[0] || 0;
|
||||||
|
default: return 0;
|
||||||
}
|
}
|
||||||
gate.value = result;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
export function evaluateAll(recordWave = false) {
|
||||||
state.gates.forEach(g => {
|
for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
|
||||||
if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0;
|
let changed = false;
|
||||||
});
|
for (const gate of state.gates) {
|
||||||
state.gates.forEach(g => evaluate(g));
|
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();
|
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
|
// Register evaluateAll in waveform to break circular dependency
|
||||||
setEvaluateAll(evaluateAll);
|
setEvaluateAll(evaluateAll);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user