From 42ecfa12a856f36d69fe59a70490690f76423422 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Sat, 14 Nov 2026 11:12:57 +0900 Subject: [PATCH 1/3] fix(editor): fit input values to the current bit width of their pin An input value is kept as it was set, so it went stale when the bit width of its pin changed afterwards, e.g. a value set while the width was not fixed (a single bit) and the pin later connected to one of 8 bits. Every time step from then on inherited the stale value, so the simulation failed in every attempt to play, and the value could not be fixed either, since the editor of an input value takes the width from the value itself. Fit the value to the current bit width of the pin when it is read: keep it if it fits, padding or dropping leading zeros, and start over from zero otherwise. `createEditorStore` is exported so that the editor store can be tested. Co-Authored-By: Claude Opus 5.5 --- src/pages/edit/Editor/store/index.tsx | 2 +- .../Editor/store/slices/core/index.test.ts | 86 +++++++++++++++++++ .../edit/Editor/store/slices/core/index.ts | 61 ++++++------- 3 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 src/pages/edit/Editor/store/slices/core/index.test.ts 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..36fb316 --- /dev/null +++ b/src/pages/edit/Editor/store/slices/core/index.test.ts @@ -0,0 +1,86 @@ +import { beforeAll, describe, expect, it, vi } from "vitest"; +import { display } 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() {} + }, + ); +}); + +/** 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); + /** Fix the bit width of the input by feeding it to a display of `x` × 1 pixels. */ + const fixInputBitWidth = (x: number) => { + const displayNode = registerTestNode(store, root.id, display.component.id, { + resolution: { x, y: 1 }, + pixelOrder: "row-major", + }); + connectTestNodePins( + store, + root.id, + input.out, + testNodePin(store, displayNode, display.inputPin.Pixels), + ); + }; + return { editorStore, input, 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, 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(input.out)).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..787a333 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -10,6 +10,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 +54,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 +63,37 @@ 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]; + // A pin whose bit width is not fixed takes a single bit, as in the simulation + const bitWidth = bitWidthStatus.isFixed ? bitWidthStatus.bitWidth : 1; + + // 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) => { From f6028e79b4f3ccbd377f94fe208e8de20bb6f6c2 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Sat, 14 Nov 2026 11:35:03 +0900 Subject: [PATCH 2/3] refactor(simulation): share the default bit width of open pins The editor fits the values it feeds to the input pins of the simulated component to the width the simulation assumes for a pin the circuit leaves open. Both hard-coded it as 1, so changing one of them would have brought back the mismatch fixed in the previous commit. Define it once as `DEFAULT_SIMULATION_BIT_WIDTH`. Co-Authored-By: Claude Opus 5.5 --- src/pages/edit/Editor/store/slices/core/index.ts | 6 ++++-- src/store/netlist.ts | 8 +++++++- src/store/simulation.ts | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index 787a333..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 { @@ -63,8 +64,9 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< "Cannot get input value for intrinsic component pin", ), ); - // A pin whose bit width is not fixed takes a single bit, as in the simulation - const bitWidth = bitWidthStatus.isFixed ? bitWidthStatus.bitWidth : 1; + 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 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( From ffec66a23a177e02e3e25b44ab9a72e389646b15 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Sat, 14 Nov 2026 11:35:03 +0900 Subject: [PATCH 3/3] test(editor): reproduce the failing simulation of a stale input value The test of simulating a circuit whose input got wider fixed the width with a display, which does not check the width of its input, so the simulation succeeded even with the stale value. Fix the width with an And of the input and a constant instead, which fails on operands of different widths as the reported circuits did. Also restore the stubbed `ResizeObserver` after the tests. Co-Authored-By: Claude Opus 5.5 --- .../Editor/store/slices/core/index.test.ts | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/pages/edit/Editor/store/slices/core/index.test.ts b/src/pages/edit/Editor/store/slices/core/index.test.ts index 36fb316..77c4cdb 100644 --- a/src/pages/edit/Editor/store/slices/core/index.test.ts +++ b/src/pages/edit/Editor/store/slices/core/index.test.ts @@ -1,5 +1,5 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; -import { display } from "../../../../../../store/intrinsics/definitions"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { and, const_ } from "../../../../../../store/intrinsics/definitions"; import { connectTestNodePins, createTestStore, @@ -22,6 +22,9 @@ beforeAll(() => { }, ); }); +afterAll(() => { + vi.unstubAllGlobals(); +}); /** A root component with an input whose bit width is not fixed yet. */ function setUp() { @@ -29,20 +32,31 @@ function setUp() { const root = registerTestComponent(store, "Root"); const input = registerTestInputNode(store, root.id, "In"); const editorStore = createEditorStore(root.id, store); - /** Fix the bit width of the input by feeding it to a display of `x` × 1 pixels. */ - const fixInputBitWidth = (x: number) => { - const displayNode = registerTestNode(store, root.id, display.component.id, { - resolution: { x, y: 1 }, - pixelOrder: "row-major", + 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, displayNode, display.inputPin.Pixels), + testNodePin(store, andNode, and.inputPin.B), ); }; - return { editorStore, input, fixInputBitWidth }; + return { editorStore, input, andOut, fixInputBitWidth }; } describe("input values", () => { @@ -71,7 +85,7 @@ describe("input values", () => { }); it("simulates a circuit whose input got wider after its value was set", () => { - const { editorStore, input, fixInputBitWidth } = setUp(); + const { editorStore, input, andOut, fixInputBitWidth } = setUp(); const key = { componentPinId: input.componentPinId, timeStep: 0 }; editorStore.getState().setInputValue(key, [true]); fixInputBitWidth(8); @@ -81,6 +95,6 @@ describe("input values", () => { const state = editorStore.getState(); expect(state.simulationError).toBeNull(); expect(state.editorMode).toBe("play"); - expect(state.getNodePinValue(input.out)).toEqual(testBits(1, 8)); + expect(state.getNodePinValue(andOut)).toEqual(testBits(1, 8)); }); });