Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/edit/Editor/store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
100 changes: 100 additions & 0 deletions src/pages/edit/Editor/store/slices/core/index.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
63 changes: 34 additions & 29 deletions src/pages/edit/Editor/store/slices/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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),
);
Expand All @@ -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) => {
Expand Down
8 changes: 7 additions & 1 deletion src/store/netlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, "SimulationNetId">;

Expand Down Expand Up @@ -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<SimulationTerminalKey, SimulationNetId>();
const instances: SimulationInstance[] = [];
Expand Down
3 changes: 2 additions & 1 deletion src/store/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import type { CCIntrinsicComponentSpec } from "./intrinsics/types";
import {
buildSimulationNetlist,
DEFAULT_SIMULATION_BIT_WIDTH,
describeInstance,
type SimulationInstance,
type SimulationNetId,
Expand Down Expand Up @@ -154,7 +155,7 @@ export default function simulateComponent(
componentId: CCComponentId,
inputValues: Map<CCComponentPinId, SimulationValue>,
previousFrame: SimulationFrame | null = null,
defaultBitWidth = 1,
defaultBitWidth = DEFAULT_SIMULATION_BIT_WIDTH,
): SimulationFrame {
const netlist = getSimulationNetlist(store, componentId, defaultBitWidth);
invariant(
Expand Down
Loading