diff --git a/js/events.js b/js/events.js index ca2eef1..e275d0e 100644 --- a/js/events.js +++ b/js/events.js @@ -100,12 +100,18 @@ export function initEvents() { if (state.connecting) { 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.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(); + console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', ')); } 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.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(); + console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', ')); } state.connecting = null; } else { @@ -131,7 +137,10 @@ export function initEvents() { const gate = state.dragging; if (gate.type === 'INPUT' || gate.type === 'CLOCK') { gate.value = gate.value ? 0 : 1; + console.log(`[toggle] ${gate.type}#${gate.id} → ${gate.value}`); 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; diff --git a/js/gates.js b/js/gates.js index a5e6e89..9f62ceb 100644 --- a/js/gates.js +++ b/js/gates.js @@ -66,9 +66,11 @@ export function getOutputPorts(gate) { return ports; } -export function evaluate(gate, visited = new Set()) { - if (visited.has(gate.id)) return gate.value || 0; - visited.add(gate.id); +/** + * 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); @@ -77,39 +79,64 @@ export function evaluate(gate, visited = new Set()) { 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 ? evaluate(srcGate, visited) : 0); + inputs.push(srcGate ? (srcGate.value || 0) : 0); } else { inputs.push(0); } } - let result = 0; if (gate.type.startsWith('COMPONENT:')) { const outputs = evaluateComponent(gate, inputs); - result = outputs[0] || 0; - } else { - switch (gate.type) { - case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break; - case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break; - case 'NOT': result = inputs[0] ? 0 : 1; break; - case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break; - case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break; - case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break; - case 'OUTPUT': result = inputs[0] || 0; break; - } + 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; } - 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) { - state.gates.forEach(g => { - if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0; - }); - state.gates.forEach(g => evaluate(g)); + 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);