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
29 changes: 29 additions & 0 deletions src/pages/edit/Editor/components/SimulationErrorAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Alert, Snackbar } from "@mui/material";
import { useComponentEditorStore } from "../store";

/**
* Shows why the circuit could not be simulated, e.g. because a loop needs a flip-flop
* to break it or because an input pin is left unconnected.
*/
export default function CCComponentEditorSimulationErrorAlert() {
const componentEditorState = useComponentEditorStore()();
const message = componentEditorState.simulationError;

return (
<Snackbar
open={message !== null}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
<Alert
severity="error"
variant="filled"
sx={{ maxWidth: "600px" }}
onClose={() => {
componentEditorState.dismissSimulationError();
}}
>
{message}
</Alert>
</Snackbar>
);
}
2 changes: 2 additions & 0 deletions src/pages/edit/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CCComponentEditorContextMenu from "./components/ContextMenu";
import CCComponentEditorFrameControl from "./components/FrameControl";
import CCComponentEditorGrid from "./components/Grid";
import { CCComponentEditorNodePinPropertyEditor } from "./components/NodePinPropertyEditor";
import CCComponentEditorSimulationErrorAlert from "./components/SimulationErrorAlert";
import CCComponentEditorTitleBar from "./components/TitleBar";
import CCComponentEditorViewModeSwitcher from "./components/ViewModeSwitcher";
import CCComponentEditorRenderer from "./renderer";
Expand Down Expand Up @@ -50,6 +51,7 @@ function CCComponentEditorContent({
<CCComponentEditorFrameControl />
<CCComponentEditorContextMenu onEditComponent={onEditComponent} />
<CCComponentEditorNodePinPropertyEditor />
<CCComponentEditorSimulationErrorAlert />
{isComponentPropertyDialogOpen && (
<ComponentPropertyDialog
componentId={componentId}
Expand Down
100 changes: 58 additions & 42 deletions src/pages/edit/Editor/store/slices/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator<
let simulationCacheKey = "";
/** index = timeStep */
let simulationCachedFrames: SimulationFrame[] = [];
/** set when the circuit cannot be simulated, to not retry on every change */
let hasSimulationFailed = false;

return {
define: (set, get) => {
Expand Down Expand Up @@ -104,11 +106,17 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator<
setRangeSelect(rangeSelect) {
set((state) => ({ ...state, rangeSelect }));
},
simulationError: null,
dismissSimulationError() {
set((state) => ({ ...state, simulationError: null }));
},
setEditorMode(mode) {
hasSimulationFailed = false;
set((state) => ({
...state,
editorMode: mode,
playbackMode: "manual",
simulationError: null,
}));
},
setTimeStep(timeStep: number) {
Expand Down Expand Up @@ -152,12 +160,10 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator<
}));
},
getNodePinValue(nodePinId: CCNodePinId): SimulationValue | undefined {
const { nodeId } = nullthrows(store.nodePins.get(nodePinId));
return nullthrows(
nullthrows(simulationCachedFrames[get().timeStep]).nodes.get(
nodeId,
),
).pins.get(nodePinId);
simulationCachedFrames[get().timeStep],
`Time step ${get().timeStep} has not been simulated`,
).getNodePinValue(nodePinId);
},
getComponentPinValue(
componentPinId: CCComponentPinId,
Expand All @@ -175,52 +181,62 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator<
const executeSimulation = () => {
if (editorStore.getState().editorMode !== "play") return;

const newSimulationCacheKey =
store.nodes
.getMany()
.map((node) => node.id)
.join() +
store.nodePins
.getMany()
.map((nodePin) => `${nodePin.id}_${nodePin.manualBitWidth || 0}`)
.join(",") +
store.connections
.getMany()
.map((connection) => connection.id)
.join() +
[...editorStore.getState().inputValues.entries()]
.map(([key, value]) => key + value.join())
.join();
// The netlist is cached against the revision of the store, so the key only
// has to cover that revision and the values fed to the input pins.
const newSimulationCacheKey = [
store.revision,
...[...editorStore.getState().inputValues.entries()].map(
([key, value]) => `${key}${value.join()}`,
),
].join("|");
if (newSimulationCacheKey !== simulationCacheKey) {
simulationCacheKey = newSimulationCacheKey;
simulationCachedFrames = [];
hasSimulationFailed = false;
}
if (hasSimulationFailed) return;

const editorState = editorStore.getState();
let isUpdated = false;
for (
let timeStep = simulationCachedFrames.length;
timeStep <= editorState.timeStep;
timeStep += 1
) {
const previousFrame = simulationCachedFrames[timeStep - 1] ?? null;
const inputValues = new Map<CCComponentPinId, SimulationValue>();
const pins = store.componentPins.getManyByComponentId(componentId);
for (const pin of pins) {
invariant(pin.implementation);
if (pin.type === "input") {
inputValues.set(
pin.id,
editorState.getInputValue({ componentPinId: pin.id, timeStep }),
);
try {
for (
let timeStep = simulationCachedFrames.length;
timeStep <= editorState.timeStep;
timeStep += 1
) {
const previousFrame = simulationCachedFrames[timeStep - 1] ?? null;
const inputValues = new Map<CCComponentPinId, SimulationValue>();
const pins = store.componentPins.getManyByComponentId(componentId);
for (const pin of pins) {
invariant(pin.implementation);
if (pin.type === "input") {
inputValues.set(
pin.id,
editorState.getInputValue({
componentPinId: pin.id,
timeStep,
}),
);
}
}
}
simulationCachedFrames.push(
nullthrows(
simulationCachedFrames.push(
simulateComponent(store, componentId, inputValues, previousFrame),
),
);
isUpdated = true;
);
isUpdated = true;
}
} catch (error) {
// Back to edit mode: without frames the renderers have no values to show.
hasSimulationFailed = true;
simulationCachedFrames = [];
editorStore.setState((state) => ({
...state,
editorMode: "edit",
playbackMode: "manual",
timeStep: 0,
simulationError:
error instanceof Error ? error.message : String(error),
}));
return;
}
if (isUpdated) editorStore.setState((s) => ({ ...s }));
};
Expand Down
3 changes: 3 additions & 0 deletions src/pages/edit/Editor/store/slices/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export type EditorStoreCoreSlice = {
selectNode(ids: CCNodeId[], exclusive: boolean): void;
unselectNode(ids: CCNodeId[]): void;
selectConnection(ids: CCConnectionId[], exclusive: boolean): void;
/** message the last simulation attempt failed with, if it failed */
simulationError: string | null;
dismissSimulationError(): void;
getNodePinValue(nodePinId: CCNodePinId): SimulationValue | undefined;
getComponentPinValue(
componentPinId: CCComponentPinId,
Expand Down
53 changes: 49 additions & 4 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { CCStoreAutoSaver } from "./autoSaver";
import { CCComponentStore } from "./component";
import { CCComponentPinStore } from "./componentPin";
import { CCConnectionStore } from "./connection";
import {
CCComponentStore,
ccComponentStoreChangeEventTypes,
} from "./component";
import {
CCComponentPinStore,
ccComponentPinStoreChangeEventTypes,
} from "./componentPin";
import {
CCConnectionStore,
ccConnectionStoreChangeEventTypes,
} from "./connection";
import * as intrinsics from "./intrinsics/definitions";
import { CCNodeStore } from "./node";
import { CCNodePinStore } from "./nodePin";
import { CCNodePinStore, ccNodePinStoreChangeEventTypes } from "./nodePin";
import TransactionManager from "./transaction";

/**
Expand All @@ -25,6 +34,17 @@ export default class CCStore {

autoSaver: CCStoreAutoSaver;

#revision = 0;

/**
* How many circuit changes the store has seen. Anything derived from the circuit, such
* as a simulation netlist, can be cached against this number and rebuilt once it
* changes. Moving a node does not change the circuit, so it does not count.
*/
get revision(): number {
return this.#revision;
}

/**
* Constructor of CCStore
* @param rootComponent root component
Expand All @@ -45,6 +65,31 @@ export default class CCStore {
this.componentPins.register(pin);
}
}

const countRevision = () => {
this.#revision += 1;
};
for (const eventType of ccComponentStoreChangeEventTypes) {
this.components.on(eventType, countRevision);
}
for (const eventType of ccComponentPinStoreChangeEventTypes) {
this.componentPins.on(eventType, countRevision);
}
// `didUpdate` of the node store also fires for position changes, which the circuit
// does not depend on, so only the config is watched here.
for (const eventType of [
"didRegister",
"didUnregister",
"didUpdateConfig",
] as const) {
this.nodes.on(eventType, countRevision);
}
for (const eventType of ccNodePinStoreChangeEventTypes) {
this.nodePins.on(eventType, countRevision);
}
for (const eventType of ccConnectionStoreChangeEventTypes) {
this.connections.on(eventType, countRevision);
}
}

mount() {
Expand Down
83 changes: 50 additions & 33 deletions src/store/intrinsics/base.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
import { mapValues } from "es-toolkit";
import type { CCComponent, CCComponentId } from "../component";
import type { CCComponentPin, CCComponentPinId } from "../componentPin";
import type { CCNodeId } from "../node";
import type { CCNodePinId } from "../nodePin";
import type { SimulationFrame } from "../simulation";
import type { SimulationValue } from "../simulation";
import type {
CCIntrinsicComponentSpec,
CCIntrinsicComponentType,
} from "./types";

export type CCIntrinsicComponentShape<Spec extends CCIntrinsicComponentSpec> = {
inputShape: Record<Spec["in"], CCComponentPinInstanceShapes>;
outputShape: Record<Spec["out"], CCComponentPinInstanceShapes>;
};

export type CCComponentPinInstanceShapes = {
nodePinId: CCNodePinId;
bitWidth: number;
}[];

export type ComponentEvaluationContext = {
previousFrame: SimulationFrame | null;
currentFrame: SimulationFrame;
defaultBitWidth: number;
};

export type Context = {
componentId: CCComponentId;
};

/**
* How the bit width of an intrinsic component pin is determined.
* - `inferred` — inferred from the pins it is transitively connected to.
Expand Down Expand Up @@ -62,22 +40,60 @@ type IntrinsicComponentPinAttributes<Spec extends CCIntrinsicComponentSpec> = {
export type RegisteredIntrinsicComponentPinAttributes =
IntrinsicComponentPinAttributes<CCIntrinsicComponentSpec> & { key: string };

type IntrinsicComponentEvaluationFunction<
/**
* The values of the pins of one intrinsic node, grouped by pin key. A pin key can have
* more than one node pin when it is split, so each key holds the values in pin order.
*/
export type IntrinsicPinValues<Key extends string> = Record<
Key,
SimulationValue[]
>;

/** What an intrinsic node needs to know about itself besides its input values. */
export type IntrinsicEvaluationEnvironment<
Spec extends CCIntrinsicComponentSpec,
> = (
context: ComponentEvaluationContext,
nodeId: CCNodeId,
shape: CCIntrinsicComponentShape<Spec>,
config: Spec["config"],
) => boolean;
> = {
/** bit widths of the output pins, grouped like {@link IntrinsicPinValues} */
outputBitWidths: Record<Spec["out"], number[]>;
config: Spec["config"];
};

/**
* How the outputs of an intrinsic node are computed in one time step.
* - `combinational` — from the inputs of the same time step.
* - `sequential` — from the inputs of the previous time step (`null` in the first one).
* It does not see the current inputs at all, because it is evaluated before they are
* known. That is what lets a circuit contain a feedback loop through such a node.
*/
export type IntrinsicComponentBehavior<
Spec extends CCIntrinsicComponentSpec = CCIntrinsicComponentSpec,
> =
| {
kind: "combinational";
evaluate(
inputs: IntrinsicPinValues<Spec["in"]>,
environment: IntrinsicEvaluationEnvironment<Spec>,
): IntrinsicPinValues<Spec["out"]>;
}
| {
kind: "sequential";
evaluate(
previousInputs: IntrinsicPinValues<Spec["in"]> | null,
environment: IntrinsicEvaluationEnvironment<Spec>,
): IntrinsicPinValues<Spec["out"]>;
};

type Props<Spec extends CCIntrinsicComponentSpec> = {
type: CCIntrinsicComponentType;
name: string;
in: Record<Spec["in"], IntrinsicComponentPinAttributes<Spec>>;
out: Record<Spec["out"], IntrinsicComponentPinAttributes<Spec>>;
initialConfig: Spec["config"];
evaluate: IntrinsicComponentEvaluationFunction<Spec>;
/**
* null for `Input` and `Output`, which only carry a value across a component
* boundary and are resolved away before the simulation evaluates anything
*/
behavior: IntrinsicComponentBehavior<Spec> | null;
};
export class IntrinsicComponentDefinition<
Spec extends CCIntrinsicComponentSpec = CCIntrinsicComponentSpec,
Expand All @@ -90,7 +106,8 @@ export class IntrinsicComponentDefinition<
readonly inputPin: Record<Spec["in"], CCComponentPin>;
readonly outputPin: Record<Spec["out"], CCComponentPin>;
readonly initialConfig: Spec["config"];
readonly evaluate: IntrinsicComponentEvaluationFunction<Spec>;
/** @see {@link Props.behavior} */
readonly behavior: IntrinsicComponentBehavior<Spec> | null;

private static _lastIndex = 0;

Expand All @@ -114,7 +131,7 @@ export class IntrinsicComponentDefinition<
name: this.name,
};
IntrinsicComponentDefinition._byId.set(this.id, this);
this.evaluate = props.evaluate;
this.behavior = props.behavior;

this.inputPin = mapValues(props.in, (attributes, key) =>
this._registerPin("input", key, attributes),
Expand Down
Loading
Loading