diff --git a/src/components/BreakpointPanel.vue b/src/components/BreakpointPanel.vue new file mode 100644 index 000000000..08bb405f3 --- /dev/null +++ b/src/components/BreakpointPanel.vue @@ -0,0 +1,501 @@ + + + + + \ No newline at end of file diff --git a/src/components/DebugControls.vue b/src/components/DebugControls.vue new file mode 100644 index 000000000..46eb187a9 --- /dev/null +++ b/src/components/DebugControls.vue @@ -0,0 +1,210 @@ + + + + + \ No newline at end of file diff --git a/src/components/DebugTimeline.vue b/src/components/DebugTimeline.vue new file mode 100644 index 000000000..c195c570f --- /dev/null +++ b/src/components/DebugTimeline.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/src/simulator/debug/BreakpointManager.js b/src/simulator/debug/BreakpointManager.js new file mode 100644 index 000000000..9dc00e52c --- /dev/null +++ b/src/simulator/debug/BreakpointManager.js @@ -0,0 +1,214 @@ +/* eslint-disable import/no-cycle */ + +/** + * Manages breakpoints for debugging + * @category debug + */ +export class BreakpointManager { + constructor() { + this.breakpoints = [] + this.nextId = 1 + this.triggeredBreakpoint = null + } + + /** + * Add a new breakpoint + * @param {Object} config - Breakpoint configuration + * @returns {Object} The created breakpoint + */ + addBreakpoint(config) { + const breakpoint = { + id: this.nextId++, + enabled: true, + hitCount: 0, + description: this.generateDescription(config), + ...config + } + this.breakpoints.push(breakpoint) + return breakpoint + } + + /** + * Generate human-readable description + */ + generateDescription(config) { + if (config.wireId) { + const conditionText = { + 'equals': `= ${config.value}`, + 'changes': 'changes', + 'risingEdge': '0→1', + 'fallingEdge': '1→0' + }[config.condition] || config.condition + + return `Wire ${config.wireId} ${conditionText}` + } + if (config.componentId) { + return `Component ${config.componentId} ${config.condition}` + } + return 'Custom breakpoint' + } + + /** + * Remove a breakpoint by ID + */ + removeBreakpoint(id) { + this.breakpoints = this.breakpoints.filter(bp => bp.id !== id) + } + + /** + * Toggle breakpoint enabled/disabled + */ + toggleBreakpoint(id) { + const bp = this.breakpoints.find(bp => bp.id === id) + if (bp) { + bp.enabled = !bp.enabled + } + } + + /** + * Check all breakpoints - returns triggered breakpoint or null + * @param {Scope} scope - Circuit scope + * @param {Object} previousState - Previous circuit state for change detection + */ + checkBreakpoints(scope, previousState) { + for (const bp of this.breakpoints) { + if (!bp.enabled) continue + + if (this.evaluateBreakpoint(bp, scope, previousState)) { + bp.hitCount++ + this.triggeredBreakpoint = bp + return bp + } + } + return null + } + + /** + * Evaluate a single breakpoint + */ + evaluateBreakpoint(bp, scope, previousState) { + // Wire-based breakpoints + if (bp.wireId !== undefined) { + return this.evaluateWireBreakpoint(bp, scope, previousState) + } + + // Component-based breakpoints + if (bp.componentId !== undefined) { + return this.evaluateComponentBreakpoint(bp, scope) + } + + // Custom function breakpoint + if (bp.customCondition) { + return bp.customCondition(scope) + } + + return false + } + + /** + * Evaluate wire breakpoint + */ + evaluateWireBreakpoint(bp, scope, previousState) { + // Find the wire - check all nodes in the scope + let currentValue = null + let previousValue = null + + // Check all nodes to find matching wire/node + if (scope.allNodes) { + for (let i = 0; i < scope.allNodes.length; i++) { + // Simple ID check - you might need to adjust based on your wire ID system + if (i === bp.wireId || scope.allNodes[i].id === bp.wireId) { + currentValue = scope.allNodes[i].value + + // Get previous value from state + if (previousState && previousState.nodes && previousState.nodes[i]) { + previousValue = previousState.nodes[i].value + } + break + } + } + } + + if (currentValue === null) return false + + // Evaluate condition + switch (bp.condition) { + case 'equals': + return currentValue === bp.value + + case 'changes': + return previousValue !== null && previousValue !== currentValue + + case 'risingEdge': + return previousValue === 0 && currentValue === 1 + + case 'fallingEdge': + return previousValue === 1 && currentValue === 0 + + case 'greaterThan': + return currentValue > bp.value + + case 'lessThan': + return currentValue < bp.value + + default: + return false + } + } + + /** + * Evaluate component breakpoint + */ + evaluateComponentBreakpoint(bp, scope) { + // Find component by ID + // You'll need to adjust this based on how components are stored + const moduleTypes = ['Input', 'Output', 'AndGate', 'OrGate', /* add more */] + + for (const moduleType of moduleTypes) { + if (scope[moduleType]) { + for (const component of scope[moduleType]) { + if (component.id === bp.componentId) { + // Check output value + if (bp.condition === 'outputEquals' && component.output) { + return component.output.value === bp.value + } + } + } + } + } + + return false + } + + /** + * Get all breakpoints + */ + getAllBreakpoints() { + return this.breakpoints + } + + /** + * Clear all breakpoints + */ + clear() { + this.breakpoints = [] + this.triggeredBreakpoint = null + } + + /** + * Get the last triggered breakpoint + */ + getTriggeredBreakpoint() { + return this.triggeredBreakpoint + } + + /** + * Clear triggered state + */ + clearTriggered() { + this.triggeredBreakpoint = null + } +} + +// Export singleton instance +export const breakpointManager = new BreakpointManager() \ No newline at end of file diff --git a/src/simulator/debug/StateHistory.js b/src/simulator/debug/StateHistory.js new file mode 100644 index 000000000..a91fc34b5 --- /dev/null +++ b/src/simulator/debug/StateHistory.js @@ -0,0 +1,246 @@ +/* eslint-disable import/no-cycle */ +import { simulationArea } from '../src/simulationArea' + +/** + * Manages simulation state history for time-travel debugging + * @category debug + */ +export class StateHistory { + constructor(maxSize = 1000) { + this.states = [] + this.currentIndex = -1 + this.maxSize = maxSize + } + + /** + * Capture current circuit state from global scope + * @param {Scope} scope - the circuit scope to capture + * @returns {Object} The captured state + */ + captureState(scope) { + const state = { + timestamp: Date.now(), + timePeriod: simulationArea.timePeriod, + clockState: simulationArea.clockState, + scopeId: scope.id, + scopeName: scope.name, + // Capture all circuit elements + elements: this.captureElements(scope), + // Capture all nodes + nodes: this.captureNodes(scope), + // Capture all wires + wires: this.captureWires(scope), + } + + // If we're not at the end, remove future states (branching timeline) + if (this.currentIndex < this.states.length - 1) { + this.states = this.states.slice(0, this.currentIndex + 1) + } + + this.states.push(state) + + // Maintain max size (circular buffer) + if (this.states.length > this.maxSize) { + this.states.shift() + } else { + this.currentIndex++ + } + + return state + } + + /** + * Capture all circuit elements (gates, components, etc.) + */ + captureElements(scope) { + const elements = {} + + // Iterate through all module types + const moduleList = [ + 'Input', 'Output', 'AndGate', 'OrGate', 'NotGate', 'XorGate', + 'NandGate', 'NorGate', 'XnorGate', 'Clock', 'Splitter', + 'SubCircuit', 'ConstantVal', 'BitSelector', 'Multiplexer', + 'Demultiplexer', 'TTY', 'Rom', 'Ram', 'Adder', + // Add more as needed from your moduleList + ] + + moduleList.forEach(moduleType => { + if (scope[moduleType] && Array.isArray(scope[moduleType])) { + elements[moduleType] = scope[moduleType].map(elem => ({ + x: elem.x, + y: elem.y, + direction: elem.direction, + objectType: elem.objectType, + // Capture output values + output: elem.output ? elem.output.value : undefined, + // Capture input values + inp: elem.inp ? elem.inp.map(i => ({ + value: i.value, + bitWidth: i.bitWidth + })) : [], + // Capture state for sequential elements + state: elem.state, + bitWidth: elem.bitWidth, + // For subcircuits + subcircuitId: elem.id, + // For special elements + data: elem.data, // ROM/RAM data + enable: elem.enable, // Clock enable + })) + } + }) + + return elements + } + + /** + * Capture all nodes (connection points) + */ + captureNodes(scope) { + if (!scope.allNodes) return [] + + return scope.allNodes.map(node => ({ + x: node.x, + y: node.y, + value: node.value, + bitWidth: node.bitWidth, + type: node.type, + highlighted: node.highlighted || false, + })) + } + + /** + * Capture all wires + */ + captureWires(scope) { + if (!scope.wires) return [] + + return scope.wires.map(wire => ({ + x1: wire.x1, + y1: wire.y1, + x2: wire.x2, + y2: wire.y2, + value: wire.node1 ? wire.node1.value : undefined, + bitWidth: wire.node1 ? wire.node1.bitWidth : undefined, + })) + } + + /** + * Restore a previously captured state + */ + restoreState(scope, state) { + if (!state) return + + // Restore clock state + simulationArea.clockState = state.clockState + + // Restore elements + this.restoreElements(scope, state.elements) + + // Restore nodes + this.restoreNodes(scope, state.nodes) + + // Note: Wires are derived from nodes, so they update automatically + } + + /** + * Restore element states + */ + restoreElements(scope, elementsState) { + for (const moduleType in elementsState) { + if (scope[moduleType] && Array.isArray(scope[moduleType])) { + elementsState[moduleType].forEach((elemState, index) => { + if (scope[moduleType][index]) { + const elem = scope[moduleType][index] + + // Restore output + if (elem.output && elemState.output !== undefined) { + elem.output.value = elemState.output + } + + // Restore inputs + if (elem.inp && elemState.inp) { + elemState.inp.forEach((inputState, i) => { + if (elem.inp[i]) { + elem.inp[i].value = inputState.value + } + }) + } + + // Restore state for sequential elements + if (elemState.state !== undefined) { + elem.state = elemState.state + } + + // Restore data for ROM/RAM + if (elemState.data !== undefined) { + elem.data = elemState.data + } + } + }) + } + } + } + + /** + * Restore node states + */ + restoreNodes(scope, nodesState) { + if (!scope.allNodes || !nodesState) return + + nodesState.forEach((nodeState, index) => { + if (scope.allNodes[index]) { + scope.allNodes[index].value = nodeState.value + scope.allNodes[index].highlighted = nodeState.highlighted + } + }) + } + + stepBack() { + if (this.canStepBack()) { + this.currentIndex-- + return this.states[this.currentIndex] + } + return null + } + + stepForward() { + if (this.canStepForward()) { + this.currentIndex++ + return this.states[this.currentIndex] + } + return null + } + + canStepBack() { + return this.currentIndex > 0 + } + + canStepForward() { + return this.currentIndex < this.states.length - 1 + } + + getCurrentState() { + return this.states[this.currentIndex] + } + + getAllStates() { + return this.states + } + + clear() { + this.states = [] + this.currentIndex = -1 + } + + jumpToState(index) { + if (index >= 0 && index < this.states.length) { + this.currentIndex = index + return this.states[index] + } + return null + } +} + +// Export singleton instance +export const stateHistory = new StateHistory() \ No newline at end of file