diff --git a/src/pages/edit/Editor/store/index.tsx b/src/pages/edit/Editor/store/index.tsx index f3dc151..590a0e7 100644 --- a/src/pages/edit/Editor/store/index.tsx +++ b/src/pages/edit/Editor/store/index.tsx @@ -9,7 +9,7 @@ import { createComponentEditorStoreCoreSlice } from "./slices/core"; import createComponentEditorStorePerspectiveSlice from "./slices/perspective"; import type { ComponentEditorStoreValue } from "./types"; -function createEditorStore(componentId: CCComponentId, store: CCStore) { +export function createEditorStore(componentId: CCComponentId, store: CCStore) { const props = { store, componentId }; const coreSlice = createComponentEditorStoreCoreSlice(props); const perspectiveSlice = createComponentEditorStorePerspectiveSlice(props); diff --git a/src/pages/edit/Editor/store/slices/core/index.test.ts b/src/pages/edit/Editor/store/slices/core/index.test.ts new file mode 100644 index 0000000..77c4cdb --- /dev/null +++ b/src/pages/edit/Editor/store/slices/core/index.test.ts @@ -0,0 +1,100 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { and, const_ } from "../../../../../../store/intrinsics/definitions"; +import { + connectTestNodePins, + createTestStore, + registerTestComponent, + registerTestInputNode, + registerTestNode, + testBits, + testNodePin, +} from "../../../../../../store/testUtils"; +import { createEditorStore } from "../.."; + +beforeAll(() => { + // The perspective slice observes the size of the renderer, which there is none of here + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); +}); +afterAll(() => { + vi.unstubAllGlobals(); +}); + +/** A root component with an input whose bit width is not fixed yet. */ +function setUp() { + const store = createTestStore(); + const root = registerTestComponent(store, "Root"); + const input = registerTestInputNode(store, root.id, "In"); + const editorStore = createEditorStore(root.id, store); + const andNode = registerTestNode(store, root.id, and.component.id); + const andOut = testNodePin(store, andNode, and.outputPin.Out); + /** + * Fix the bit width of the input by ANDing it with a constant of `bitWidth` ones. And + * requires both operands to be equally wide, so a stale input value fails it. + */ + const fixInputBitWidth = (bitWidth: number) => { + const constant = registerTestNode(store, root.id, const_.component.id, { + mode: "binary", + data: new Array(bitWidth).fill(true), + }); + connectTestNodePins( + store, + root.id, + testNodePin(store, constant, const_.outputPin.Out), + testNodePin(store, andNode, and.inputPin.A), + ); + connectTestNodePins( + store, + root.id, + input.out, + testNodePin(store, andNode, and.inputPin.B), + ); + }; + return { editorStore, input, andOut, fixInputBitWidth }; +} + +describe("input values", () => { + it("fits a value set before the bit width of the pin was fixed", () => { + const { editorStore, input, fixInputBitWidth } = setUp(); + const key = { componentPinId: input.componentPinId, timeStep: 0 }; + editorStore.getState().setInputValue(key, [true]); + + fixInputBitWidth(8); + + expect(editorStore.getState().getInputValue(key)).toEqual(testBits(1, 8)); + // A later time step inherits the value, fitted the same way + expect( + editorStore.getState().getInputValue({ ...key, timeStep: 3 }), + ).toEqual(testBits(1, 8)); + }); + + it("starts over from zero when a value does not fit the new bit width", () => { + const { editorStore, input, fixInputBitWidth } = setUp(); + const key = { componentPinId: input.componentPinId, timeStep: 0 }; + editorStore.getState().setInputValue(key, testBits(0b100, 3)); + + fixInputBitWidth(2); + + expect(editorStore.getState().getInputValue(key)).toEqual(testBits(0, 2)); + }); + + it("simulates a circuit whose input got wider after its value was set", () => { + const { editorStore, input, andOut, fixInputBitWidth } = setUp(); + const key = { componentPinId: input.componentPinId, timeStep: 0 }; + editorStore.getState().setInputValue(key, [true]); + fixInputBitWidth(8); + + editorStore.getState().setEditorMode("play"); + + const state = editorStore.getState(); + expect(state.simulationError).toBeNull(); + expect(state.editorMode).toBe("play"); + expect(state.getNodePinValue(andOut)).toEqual(testBits(1, 8)); + }); +}); diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index e0b0b9d..344af20 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -2,6 +2,7 @@ import nullthrows from "nullthrows"; import invariant from "tiny-invariant"; import type { CCComponentPinId } from "../../../../../../store/componentPin"; import type { CCConnectionId } from "../../../../../../store/connection"; +import { DEFAULT_SIMULATION_BIT_WIDTH } from "../../../../../../store/netlist"; import type { CCNodeId } from "../../../../../../store/node"; import type { CCNodePinId } from "../../../../../../store/nodePin"; import type { @@ -10,6 +11,7 @@ import type { } from "../../../../../../store/simulation"; // import type { CCComponentId } from "../../../../../../store/component"; import simulateComponent from "../../../../../../store/simulation"; +import { fitSimulationValue } from "../../../components/ValueEditor/value"; import type { ComponentEditorSliceCreator } from "../../types"; import { type EditorStoreCoreSlice, @@ -53,30 +55,6 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< /** @private */ inputValues: new Map(), getInputValue(inputValueKey: InputValueKey) { - // If value exists for the current time step, return it - const value = get().inputValues.get( - serializeInputValueKey(inputValueKey), - ); - if (value) return value; - - // Inherit the latest earlier input without updating the store during simulation. - const prefix = `${inputValueKey.componentPinId}:`; - let latestTimeStep = -1; - let previousValue: SimulationValue | undefined; - for (const [key, value] of get().inputValues) { - if (!key.startsWith(prefix)) continue; - const timeStep = Number(key.slice(prefix.length)); - if ( - timeStep < inputValueKey.timeStep && - timeStep > latestTimeStep - ) { - latestTimeStep = timeStep; - previousValue = value; - } - } - if (previousValue) return previousValue; - - // If not found, initialize the value based on the bit width of the pin const componentPin = nullthrows( store.componentPins.get(inputValueKey.componentPinId), ); @@ -86,11 +64,38 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< "Cannot get input value for intrinsic component pin", ), ); - return bitWidthStatus.isFixed - ? // If the bit width is fixed, initialize the value with the specified bit width - new Array(bitWidthStatus.bitWidth).fill(false) - : // If the bit width is not fixed, initialize the single-bit value as false - [false]; + const bitWidth = bitWidthStatus.isFixed + ? bitWidthStatus.bitWidth + : DEFAULT_SIMULATION_BIT_WIDTH; + + // The value set for the current time step, or else the latest earlier one, + // inherited without updating the store during simulation + let value = get().inputValues.get( + serializeInputValueKey(inputValueKey), + ); + if (!value) { + const prefix = `${inputValueKey.componentPinId}:`; + let latestTimeStep = -1; + for (const [key, previousValue] of get().inputValues) { + if (!key.startsWith(prefix)) continue; + const timeStep = Number(key.slice(prefix.length)); + if ( + timeStep < inputValueKey.timeStep && + timeStep > latestTimeStep + ) { + latestTimeStep = timeStep; + value = previousValue; + } + } + } + + // The bit width may have changed since the value was set, e.g. by connecting + // the pin to one of a fixed width. Keep the value if it fits the new width, + // and start over from zero if it does not. + return ( + (value && fitSimulationValue(value, bitWidth)) ?? + new Array(bitWidth).fill(false) + ); }, setInputValue(inputValueKey: InputValueKey, value: SimulationValue) { set((state) => { diff --git a/src/store/netlist.ts b/src/store/netlist.ts index 120b81d..1404551 100644 --- a/src/store/netlist.ts +++ b/src/store/netlist.ts @@ -43,6 +43,12 @@ import { ccIntrinsicComponentTypes } from "./intrinsics/types"; import type { CCNode, CCNodeId } from "./node"; import type { CCNodePinId } from "./nodePin"; +/** + * Bit width of a pin of the simulated component that the circuit leaves open. The values + * fed to such an input pin must be this wide, so whoever supplies them uses it as well. + */ +export const DEFAULT_SIMULATION_BIT_WIDTH = 1; + /** Identity of a net: the key of the terminal driving it. */ export type SimulationNetId = Tagged; @@ -177,7 +183,7 @@ function resolveInstanceDefaultBitWidth( export function buildSimulationNetlist( store: CCStore, componentId: CCComponentId, - defaultBitWidth = 1, + defaultBitWidth = DEFAULT_SIMULATION_BIT_WIDTH, ): SimulationNetlist { const netByTerminal = new Map(); const instances: SimulationInstance[] = []; diff --git a/src/store/simulation.ts b/src/store/simulation.ts index bcadc28..3d8f682 100644 --- a/src/store/simulation.ts +++ b/src/store/simulation.ts @@ -11,6 +11,7 @@ import type { import type { CCIntrinsicComponentSpec } from "./intrinsics/types"; import { buildSimulationNetlist, + DEFAULT_SIMULATION_BIT_WIDTH, describeInstance, type SimulationInstance, type SimulationNetId, @@ -154,7 +155,7 @@ export default function simulateComponent( componentId: CCComponentId, inputValues: Map, previousFrame: SimulationFrame | null = null, - defaultBitWidth = 1, + defaultBitWidth = DEFAULT_SIMULATION_BIT_WIDTH, ): SimulationFrame { const netlist = getSimulationNetlist(store, componentId, defaultBitWidth); invariant(