Issie logo Issie

global Namespace

Modules Description

AppMessages

 The words Issie says to the user, where they are long enough to be worth reading as prose.

 WHAT IS HERE. The Info window's four written tabs, the waveform simulator's five help panels,
 the memory help, the twenty field explanations in the Properties pane, and the confirmation
 popups whose body is several sentences rather than one - thirty-five messages, about 25,000
 characters. They were spread through the view functions that show them, written as React
 element trees with the words threaded between `str` and `bSpan` and `li` - fine to render and
 very hard to read, which is a problem for text whose only job is to read well. Written as
 markdown here, the whole of Issie's long-form help can be read end to end and reviewed as
 writing.

 WHAT IS NOT HERE, and should not be moved in.

 - Short labels, button captions and headings. A caption is easier to judge beside the button it
   sits on than in a list of thirty other strings.
 - The Catalogue's component tooltips. They are short, they are one per component, and each sits
   beside the component type it describes, which is what makes them easy to keep true.
 - One-line confirmation bodies - "The current sheet has unsaved changes." A sentence that short
   belongs with the buttons it is explaining.
 - `failwithf` and `Log` messages. They are addressed to whoever is maintaining Issie, not to
   the user, and belong at the point that fails.
 - Anything assembled from a design: the components in a combinational loop, the ports of a
   mismatched instance. The sentence around such a list can live here; the list cannot.
 - The Keyboard Shortcuts tab, which is generated from the shortcut table so that it cannot
   drift from the keys that actually fire.

 THE RISK THIS RUNS. Text kept away from the code that shows it goes stale - Issie has had that
 exact bug, a help menu item whose name had drifted from the panel it opened, so that choosing
 it said "Feature not explained". What limits it is that only the words moved: which message to
 show, and when, stays at the point that shows it, so a message cannot be reached by a route
 this module knows nothing about. Every entry also carries a comment saying what it is and where
 it appears, which is what makes an entry that no longer matches its use site noticeable while
 reading.

 That is weaker than a check. `MarkdownTests` reads every message here and fails on one that
 will not render, but nothing yet fails on an entry that has quietly stopped being used, or on a
 comment that has stopped being true. Both would be worth adding - the first is a source scan
 for each entry's name, of the kind `SourceHygiene` already does for printf.

 Markdown is rendered by `Markdown.render` - see that module for the subset supported. Tooltips
 are the exception: they are drawn by CSS from a `data-tooltip` attribute, which can hold text
 and nothing else, so tooltip entries are plain sentences with no markup.

ArrayElaborate

ArrayExpand

ArraySheetView

BlockHelpers

Bridge (Module)

The main-process half of Issie's renderer bridge. Every operation the renderer is allowed to ask the operating system for is registered here, as a named channel with a fixed shape. The renderer half is src/Preload/preload.js, and the two are meant to be read together: nothing should be exposed there that is not answered here, and nothing answered here should take a request general enough to stand in for `require`. Handlers use event.returnValue, the synchronous form, because the renderer's file wrappers return values from inside Elmish update and cannot await. get-user-data in Main.fs already worked this way before any of this existed.

Bridge (Module)

The renderer's half of the bridge to the main process. Partner to src/Preload/preload.js and src/Main/Bridge.fs. This module is the only place the renderer reads anything the operating system knows, and it exists so that the rest of the code asks for `Bridge.staticDir` rather than reaching for `__static`, `process.platform` or `@electron/remote` - none of which survive contextIsolation. Everything here is a value rather than a function. These are constants for the life of the process, so the preload fetches them once and this module just reads the result: no round trip, no ordering to get wrong, and five operations that never have to exist on the bridge at all.

BuildUartHelpers

The debug UART, as the draw block sees it. These were direct calls into IS-uart.js, imported into the renderer bundle. That file requires the native `usb` module, which a renderer without node integration cannot load at all, so it now lives in the main process and these are messages to it - see src/Main/Bridge.fs. The signatures are unchanged, which is the whole reason this port was cheap: the operations were already asynchronous, already took nothing but a viewer count, and already shared no objects with their caller. Nothing in SheetUpdate.fs had to change.

BuildView

BusWidthInferer

BusWire

BusWireRoute

BusWireRoutingHelpers

BusWireSeparate

BusWireUpdate

BusWireUpdateHelpers

CanvasExtractor

CanvasStateAnalyser

CatalogueView

CodeEditorHelpers

CommonTypes

ComponentLibraries

ComponentSlots

ConstraintReduceView

ContextMenuBuilder

Building and popping up the right-click menus, for the main process only. The menu DATA - which menus exist and what is on them - stays in ContextMenus.fs, which both processes compile: the renderer needs it to ask for a menu by name and to recognise the item that comes back. Only the part that builds an Electron menu is here, because it needs the electron module, and requiring that is what a renderer without node integration cannot do.

CustomCompPorts

DevHarness

 Driving Issie from outside it, for development and for automated checking.

 Published as `window.issieDev` in a debug build, and reached from a terminal through
 `scripts/drive.js`. It exists because the alternative - synthesising DOM events and reading
 rendered text back - is both slow and wrong often enough to mislead: a click has to find the
 right element, a wait has to guess how long a render takes, and text scraped from the DOM can
 be a frame out of date while looking authoritative.

 Four things, which is what that experience says are needed:

   onNextRender   when the update has been applied AND the view has run, so a caller can wait
                  for the app rather than for a stopwatch
   send           a named command, dispatched as the message the UI would have sent
   state          what the app currently is, as data rather than as rendered text
   simRefs        what is holding a simulation, which is the question the DOM cannot answer

 `send` takes a NAME from a fixed table rather than a serialised Msg. A Msg is an F# union
 carrying models, canvases and functions; nothing useful survives a round trip through JSON, and
 a general dispatch-anything surface in a debug build is a hazard for the sake of messages
 nobody wanted to send. Add a row to `commands` when a new one is needed - that is the point.

DiagramMainView

DiagramStyle

DrawHelpers

DrawModelType

Editor

ElectronModule

The `electron` module itself, for the main process only. These bindings used to live in ElectronAPI.fs, which both projects compile - the renderer needs its several hundred type definitions. But a binding is an import, and `import * from "electron"` runs when the file loads, so the renderer was requiring electron simply by using its types. A renderer without node integration cannot do that, and it is not a lazy failure: it throws as the bundle loads, before any of Issie runs. So the types stayed shared and the module came here, where requiring it is exactly right. If the renderer ever appears to need one of these values again, that is the signal that something wants a bridge operation instead.

ErrorCheck

ErrorCheckHelpers

ErrorCheckProcedural

ErrorDisplay

How an error message is put on screen when the user may need to send it to us. Issie has two kinds of error. Most are about the design - a port with nothing driving it, two labels with one name - and the user reads them, fixes the design and never thinks about them again. A few are about ISSIE: a sheet file that cannot have been produced by the editor, a simulator that hit a state it says is impossible, a check that admits it should not have fired. Those say "please report it (Info -> Bug Reports)", and a report is only useful if it carries the message. **Which is which is not written down anywhere, and this deliberately does not ask.** Classifying `SimulationErrorType` case by case would be a second list to keep in step with the first, and it would go stale the moment a case was added. Length decides instead, and it decides correctly by construction: every message that asks to be reported is long, because it has to explain that the fault is Issie's and say what to do about it. A one-line message about a missing connection stays exactly as it was. Being long is also, on its own, a reason to make a message copyable - a user quoting a two-hundred-character explanation in a forum question retypes it or drops half of it - so the rule needs no exception for the design errors it also catches.

EvalAlgebraic

EvalCompiled

 Per-component reducers, chosen and bound once when the simulation is built.

 fastReduce works out afresh, for every component of every clock step, things that were
 settled when the simulation was built: which component type this is, whether it is on the
 uint32 or the bigint path, which of its inputs are bigints, what its bus masks are, and
 which arrays its ports live in. A reducer built here has all of that already: it closes
 over the step arrays themselves and over the constants, and its body is only the work that
 actually differs from step to step.

 reducerFor returns None for a component type it does not handle, and the caller falls back
 to fastReduce. So this file can be filled in a type at a time, and the fallback is always
 the definition of what a reducer must do.

 Two rules the closures depend on, both guaranteed by the caller (see installReducers):

  - The step arrays captured here must be the ones the simulation actually uses. Reducers
    are therefore installed after every re-linking pass, including the one
    addWavesToFastSimulation does for custom components, and only for components that are
    reduced. Capturing an array that is later re-pointed would silently simulate the wrong
    signal.
  - Values in the step arrays are already within their bus width (the masking invariant), so
    a reducer masks its result exactly when its own operation can overflow, and reads
    without masking. On the uint32 path a width of exactly 32 needs care: 1u <<< 32 is 1u.

EvalKernel

EvalReference

EvilHoverCache

ExceptionReport

What the user sees of everything that has gone wrong this session. Two things use this. The Bug Reports tab shows the panel below, so that somebody who has been asked for details can get them without opening dev tools - which the checklist beside it asks them to do, and which most people reporting a bug will not manage. And the renderer's exception boundary shows `crashPage` when the view itself throws, which is the one case where the Info window cannot be opened to read the panel. **The text is the deliverable.** Everything here exists to get `Log.problemReport ()` into a forum post or an email, so the panel is a block of text and a button that copies it, and not a table of exceptions with columns. The same text is on `window.issieLog.report()`, so a developer sitting at the machine reads exactly what a user would have sent.

FastBuild

FastCreate

FastExtract

FastOrder

FastRun

FastValidate

FilesIO

FileUpdate

GraphBuilder

GraphMerger

HashMap

Helpers

Ids

The id types, and nothing else. A file of their own, compiled before everything else, for one reason: ParameterTypes holds a component id and could not name the type - which its own comment said for as long as the two were in the wrong order. Ids depend on nothing, so this is where they belong. [], like Shared/ListPairs.fs, so that `ComponentId` resolves in every file that already says `open CommonTypes` and nothing has to be changed to find it.

JSHelpers

KeyBindings

Turns a key press into an action, using the table in KeyTypes and the context the user is in. This is the only place in Issie that should listen for keys. It replaces five mechanisms that grew independently and never agreed with each other: the Electron menu accelerators, the document.onkeydown reassigned on every render in SheetDisplay, the addEventListener subscription in Renderer, the one React handler in SelectedComponentView, and the arrow-key intercept in Update. KeyTypes says what the shortcuts are; this module says what they do and delivers them.

KeyCaps

Keys drawn as keys. One place, this early in the build, because everything that tells the user which keys to press draws them the same way: the shortcut list in Info, the dialogs that name a chord, and the help text, which is markdown and so cannot reach any module that knows about the model. The appearance is `keyCap` in extra.css; what is here is the arrangement.

KeyTypes

The one place every keyboard shortcut in Issie is described. This module is deliberately pure: no Fable.React, no ElectronAPI, no ModelType, no Browser.Dom. It says *what* the shortcuts are, not what they do and not how they are delivered. The binding from an identity to an action lives in UI/KeyBindings.fs, which is the only module that needs the model. Keeping the two apart is what lets the help table, the renderer menus and (later) the context menus all be generated from one list instead of drifting apart, which is what happened to the hand-written table in UIPopups.fs. Platform is always a parameter, never read here, so this module has no dependency on how the host discovers it.

LookupArray

A growable, index-addressed store: items are added in creation order, each one stamped with the index it was stored at, and read back by that index. It exists because the simulator's build phase was indexing its own components by structural keys. A `Map` - a component id and its access path - costs a boxed comparison of the id and one of the list per lookup - 200,000 lookups into a 10,000-entry map measured 77.2 ms as such a Map and 0.2 ms as an array index - and the build does millions of them. The fix is not a faster map but an index: the thing being looked up is created by the same walk that later reads it, so it can be given its position as it is made. Nothing past `Count` is ever read, which is what lets the backing array be oversized without an `option` per slot - the codebase forbids nulls, and `Array.zeroCreate` on a reference type yields them. `Count` is the whole of the contract that keeps them out of sight.

Main

Markdown

A small markdown renderer for Issie's in-app help. Issie's longer help text - the Info window, the waveform simulator's help panels, the bodies of confirmation popups - used to be written as React element trees: `str` and `bSpan` and `li` interleaved with the words, several hundred lines of it. Read as prose it was unreadable, which is a problem for text whose whole job is to read well. It is now written as markdown in AppMessages and rendered here. The subset is not a guess: it is what that text was already doing, counted. Headings, bold, italic, inline code, links, bullet and numbered lists, and tables - which is core GitHub markdown with nothing left over. There are no images and no nested lists, so there is no support for them; a line that tries will render as its own text rather than silently vanish. One addition to that subset: [[Ctrl 0]] draws the keys of a chord as keys. Help text that tells the reader to press something should say it the way the rest of Issie does, and bold prose in the middle of a sentence is not that. TWO THINGS THIS DELIBERATELY DOES NOT DO. It does not produce an HTML string. Rendering markdown by handing HTML to `dangerouslySetInnerHTML` is the usual shortcut, and in a renderer with node integration, for messages that interpolate sheet names the user chose, it is a bad trade. `render` builds React elements, so there is no path from message text to executed markup. It does not open links itself. A markdown link becomes an anchor with an `onClick` supplied by the caller, because in Electron an ordinary `href` navigates the application window away from Issie. Keeping the handler out here also keeps this module free of Electron and of anything above it in compile order. `parse` is pure and returns data, so the tests can read every message in AppMessages under plain .NET without a browser. `render` is the only part that needs one.

MemoryData

MemoryEditorView

MenuHelpers

MiscMenuView

ModelHelpers

ModelType

NearleyBindings

Notifications

NumberHelpers

Optics

ParameterAnalysis

ParameterTypes

ParameterView

PathHelpers

Pure path arithmetic, shared by the Fable and .NET builds. These were node's `path` under Fable and System.IO.Path under .NET - two implementations that already disagreed (node's join normalises, Path.Join does not), and one of which disappears with contextIsolation since `path` is a node module the renderer will not be able to require. Joining strings is not a privileged operation and has no business crossing a process boundary, so rather than becoming ten more bridge calls at 189 call sites this is one implementation for both targets. It follows node's semantics, which is what the app was built against, and Tests/Issie.Tests/PathHelperTests.fs holds it to them. Separator handling is platform-dependent in the way node's is: on Windows both `/` and `\` divide segments and output uses `\`; on POSIX only `/` divides and a backslash is an ordinary filename character. The leading root - a drive, a UNC share, or `/` - is found but never rewritten by dirname and basename, because node does not rewrite it either, and UNC paths matter here: Issie is run from networked locations on cluster machines.

Playground

PopupHelpers

PortData

The port slices of one sidecar build, held for the renderer to derive instance views from. The same shape as `WaveData` and `StepPanelData` and for the same reason: view code reads synchronously, on every render, and a separate process cannot be asked synchronously - so the update function fills this and the views read it (the diagram at the top of SimInterface.fs). What is held is small and immutable by construction. For one simulation the circuit is frozen: the design the sidecar simulates is exactly the design the renderer holds, and draw-block edits touch neither until a refresh builds anew. So a slice is fetched once per (build, instance), never invalidated, never re-fetched - the whole store simply dies with its build. And the instances held are the ones the UI references - the selection's, the top sheet, the ones the selector shows - bounded by what is on screen, never by the expansion. Held rather than modelled for the reason docs/mutableState.md allows: a read-through memo of another process's answers, written from inside the promise that fetched them.

PortView

 The ports of one elaborated sheet instance that a waveform can be taken of.

 **This is the narrow question the renderer should be asking a simulator**, and the one
 `SimInterface` describes: not "give me every wave in the design" - which is proportional to the
 expansion and is what a remote simulator exists to keep out of the renderer - but "which ports
 does THIS instance offer", asked of the handful of instances on screen.

 Everything here that a design could answer is answered from the design instead; what is left is
 what only an elaborated simulation knows. That is two things, and they are the reason this
 cannot be a design-time query:

   the WIDTH of a port, because parameters are resolved when a design is elaborated, so two
   instances of one sheet are meant to differ;

   where the port's data LIES - its driver and its step array - which is a fact about one build
   and is invalidated by the next.

 Below the UI for the same reason `RamView` and `WaveNames` are: the .NET sidecar has a
 FastSimulation of its own and must answer this from exactly this code, or the two agree until
 they do not.

PropertiesHelp

What the fields in the Properties pane mean, and the label element that says so. The Catalogue explains every component before you place it, in a tooltip written as a sentence. The pane where you then configure the component you placed explained nothing at all: it showed "Width (bits)", "Optional Ports", "LSB" and left the user to work out the rest. This closes that gap without adding prose to the pane itself, which is short on room. The text is keyed by the label the field displays, so a field acquires its explanation simply by being labelled - no call site has to pass one, and the same label used in two places is explained the same way in both. A label with no entry here renders exactly as it did before. Keep each entry to what the field does and, where there is one, the thing people get wrong.

RamData

Asking the .NET simulator for what a RAM table draws. **There is no cache here, and there does not need to be.** The rows are small - at most a hundred a table - so they live in the model, in `WaveSimModel.RamRows`, and the view reads them the way it reads everything else. That is not a stylistic preference: the waveform pane is memoised on the model, so a reply landing in a module of its own changes nothing the renderer can see, and the table stays empty until something unrelated happens to redraw it. Held in the model, arriving IS a redraw. The waveform data proper is the case that genuinely cannot do this and lives outside (`WaveData`): megabytes of typed arrays, read per render, per wave. What is left here is the asking - which belongs in the update function and not in a render, so that a request is made when the question changes rather than on every frame.

RamStore

 How a read/write memory's contents are held while a simulation runs.

 See [docs/dev/ramRepresentation.md] for why this exists and what it is worth. In short: the
 contents used to be a `Map` and a fresh snapshot of it was stored for every
 clock step, so a write cost an AVL path copy plus two boxed `BigInt`s, a read cost a descent
 with boxed comparisons, and a long waveform simulation retained every version it had ever
 made. Here

 - a read during simulation is an array index,
 - a write appends two numbers to shared flat arrays,
 - the value an address held at any past step is a binary search of that address's own writes,
   whose cost does not depend on how far the cursor moved, and
 - nothing at all is allocated per step.

 **Storage is CSR.** Every address that is ever written is given a *slot* number, and a slot's
 writes are one contiguous run of the shared `IStep`/`IVal` arrays, delimited by `Start`. The
 alternative - a growable list per address - costs a record and two list objects per address,
 which is around 230 bytes for what may be a single 8-byte write. That is affordable for one
 big RAM and is not for a hundred small ones, which between them have just as many addresses.
 Recent writes go to a small tail and are folded in by `compact`.

 **Where this file sits.** F# compile order is the dependency layering, and
 `SimGraphTypes.SimulationComponentState` names this type, so it has to be compiled before
 `SimGraphTypes.fs` and cannot live in the `FastSim` block with the rest of the simulator. It
 depends on nothing but `CommonTypes`, which is what makes that possible.

 **Mutability.** This is the same layer as the step arrays, which are already mutable, and
 [docs/mutableState.md] allows it for a measured performance reason - the whole point here is
 to stop allocating. Nothing mutable escapes: `Memory1` remains the immutable type everywhere
 outside the running simulation, and `toMemory` builds one on demand.

RamView

The rows a RAM table shows at one clock, worked out from a simulation that holds the memory. Below the UI because two simulators produce it: the renderer's own, and the .NET sidecar, which has a `FastSimulation` of its own and answers `SimReadRam` from exactly this code. That is the point of the module - the alternative is the sidecar reimplementing which row counts as read and which as written, and two answers that agree until they do not. `SimInterface` named the obstacle: reading a RAM "needs a row type that is declared in the waveform UI and has to move first". This is that move. `WaveSimStyle` keeps the styling. What is NOT here is display: the radix, the padding, the comment column, and the collapsing of a run of zero locations into one "0x0100 ... 0x01FF" row. Those are the table's own business and stay in `WaveSimRams`, which is why this returns locations and values rather than strings.

Renderer

RotateScale

SelectedComponentView

Sheet

SheetCreator

SheetDescription

SheetDisplay

SheetLayout

SheetSnap

SheetUpdate

SheetUpdateHelpers

SidecarClient

 The renderer's half of the sidecar transport: connect, then correlated request -> response.

 Wire protocol, shared with src/Sidecar/Protocol.fs - the two files change together:

     byte 0        command; a response carries the request's command with bit 7 set, and bit 6
                   as well when its payload is an error message rather than the answer
     bytes 1..4    correlation id, uint32 little-endian, echoed back by the sidecar
     bytes 5..7    padding, always zero: an 8-byte header means a binary response payload
                   starts 8-aligned, so Uint32Array/Float64Array views need no copy
     bytes 8..     payload

 The socket is the browser WebSocket global: the renderer is sandboxed and a browser API is
 what it has - which is also why no npm package is involved. Where the socket points comes
 from Bridge.sidecarEndpoint, polled at the moment of connecting.

 Frames are Uint8Array over ArrayBuffer, reached only through the Emit helpers below: sizes
 and correlation ids stay as plain floats (JS numbers), and nothing indexes a typed array
 through an F# array type.

SidecarSession

The one simulation session the sidecar holds, and the renderer's picture of it. The sidecar simulates one design at a time - `SimSession` keeps a single session and every build replaces it - so there is one place here that knows what it is holding, rather than one per feature. The waveform simulator and the step simulator both draw on this, and because they share it neither can build over the other's session without the other finding out: a command naming a session the sidecar no longer holds is refused by name. This module is deliberately below the UI. It was extracted from `WaveProvider` when the step simulator needed the same two operations, and `SimulationView` compiles long before that. **None of this is model state.** It is what a separate process is believed to hold, which the model cannot know and has no better place for (docs/mutableState.md). WHICH simulator is running IS a model fact - `Model.SimulateInRenderer` - and is passed to the callers rather than mirrored here, because a second copy of a model fact is a thing that can disagree with it.

SimDigest

A deterministic text rendering of a design's observable simulation behaviour, shared by both runtimes so they can be compared byte for byte. This is the golden-model render, moved here from the test suite unchanged so that the dotnet sidecar can produce the SAME text the tests pin in their .golden files: every top-level input driven by the deterministic stimulus, every output, viewer and clocked component printed per cycle, RAM contents at the end. Electron computes it locally and the sidecar answers the SimDigest protocol command with its own; any difference is a real cross-runtime divergence, located by the first differing line.

SimGraphTypes

SimInterface

 What the waveform viewer and the step simulator ask of a simulator, and what a simulator
 promises them - whichever process it runs in.

 **Make more of the system synchronous rather than accept asynchrony and manage it.** Effort
 spent moving work to where it can be answered at once is repaid; complexity taken on to
 tolerate an answer that is not there yet never goes away. This is the Elmish discipline, and it
 holds up surprisingly well in Issie - with memoisation where recomputing would be too slow, and
 a few deliberate breaks in places where it does not matter. A simulator in another process is
 the hardest thing to hold to it, and it can still be held: what is genuinely slow becomes a
 message to start and a message on completion, with a state in the model between the two, and
 everything else stays a question with an answer. That takes care and judgement rather than a
 blanket rule, which is why it is written down here rather than assumed.

 **Interrogating a simulator is synchronous, and there is no cache between.**

 Almost everything the UI asks a simulator is a small question about structure - which instances
 are inside this one, which ports does this instance offer, what is this port called and how
 wide is it - and the answer is wanted from inside `view`, where nothing can await.

 Only three things take long enough that they cannot be answered while a frame is being drawn,
 and none of them is a question:

   PRE-BUILD, where there is no simulation yet to ask;
   BUILD, which elaborates the design;
   RUN, which simulates it.

 All three are commands, the renderer issues them, and so the renderer knows perfectly well when
 it is in one. That is the whole of the problem: every synchronous interrogation answers
 immediately in those three states - nothing yet - and the UI draws what it has, which is what
 it must do anyway while a build is running. Everywhere else the answer is there to be given.

 So there is no read-through cache, no per-question "not yet", and no await in view code. An
 earlier design here had all three, on the assumption that talking to another process must make
 every question asynchronous. It does not: what makes a question asynchronous is being unable to
 answer it, and the three states above are exactly the ones where that is true and are known in
 advance.

 **What makes that possible is WHERE the answers live.** A question the renderer answers from
 what it holds is synchronous by construction. It holds two things:

   the DESIGN (`SimTypes.SimulatedDesign`), which settles every question about structure - which
   sheet an instance is a copy of, what it is labelled, what is inside it, how many of it there
   are - all by walking the design, none by expanding it;

   and what the last BUILD produced about ports: which of them carry a waveform, what each is
   called, and how wide it is. Widths are why this cannot come from the design alone - a
   parameterised sheet resolves them per instance - but they are settled by the build, so they
   are known before the first frame that asks.

 **Nothing displayed yet is a fine answer**, because it becomes a displayed one. A waveform with
 no data draws nothing and is drawn again when its data lands; where the sidecar simulates that
 is every view until the first fetch returns. WaveSimSVGs goes further and draws the last window
 that ARRIVED rather than freezing on the last one that matched, so a fast scroll keeps moving.

 The one place a lot of asking could collide is the waveform data itself, and it does not: every
 wave a refresh is missing goes in ONE command, with at most one RAM behind it, sequentially in
 one promise, under the one FetchInProgress bit that stops the next message asking again
 (WaveSimTop.fetchWhatIsMissing). So the number of things in flight is one, whatever the number
 of waveforms - which is what keeps the reads above answerable without arbitration.

 The one thing that is genuinely per-instance AND per-build is where a port's data LIES. That is
 only ever needed twice: to issue a fetch, which happens in the update function where an await
 is already happening, and thereafter as a KEY into what the fetch returned - `Wave.DriverIndex`
 indexing `WaveData` and `WaveDrawn`, both of which are renderer-side stores of data already in
 hand. Neither is a question put to a simulator during a render.

 **Why this interface is narrow and the old one was not.** What the renderer uses today is not
 an interface at all: it is field access into a `FastSimulation` - the component store,
 `Drivers`, `WaveIndex`. Every one of those is proportional to the
 EXPANDED simulation, so promoting them to an interface would oblige a remote simulator to
 rebuild all of it renderer-side, which is the cost this exists to remove. So the interface
 asks small questions - which instances are inside this one, which ports does this instance
 offer, read me this window - and a `FastSimulation` becomes the private business of the
 implementation that has one.

 **Widths come from here and nowhere else.** Parameters are resolved when a design is
 elaborated and can change widths, so a width is a fact about the elaborated INSTANCE, not
 about the sheet it instantiates. The renderer must never infer one from a design.

 **Truth tables are not one of the two, and never will be.** They always use the renderer's own
 simulator, whatever `Model.SimulateInRenderer` says. A truth table is combinational: it builds
 one sheet with an array of two cycles (`TruthTableView`, which already asks for exactly the
 two `ModelHelpers.Constants.rendererArraySizeWhenSidecarSimulates` allows), then drives the
 inputs and reads the outputs once per ROW. That is thousands of round trips for a simulation
 small enough to have been built in the renderer anyway - the expansion the sidecar exists to
 keep out of this process is not what a truth table has. So the mutual exclusion between the
 waveform and step simulators does not extend to truth tables, and neither does this interface.

SimLog

Every simulation invocation, logged the same way in both runtimes. The hooks live inside the shared simulator code - one record per startCircuitSimulation (kind "build") and one per runFastSimulation invocation that advances the clock (kind "run") - so Electron and the dotnet sidecar cannot instrument differently. Because the renderer's progress bar works by calling runFastSimulation repeatedly with a timeout, each progress-bar update is exactly one record here, and the sidecar's chunked runs produce the same shape: any user-driven session yields directly comparable per-chunk numbers from both runtimes, pulled as JSON by the DevHarness `simLog` command on the Electron side and the SimLog protocol command on the sidecar side. A bounded ring of records rather than prints: process state, not model state (docs/mutableState.md) - written from inside the simulator, which has no dispatch, at the write frequency of a progress tick. A live line per record goes to Log.dbg Log.Sim, so the usual category switches make it visible without a rebuild.

SimpleDesignShim

SimpleDesign back to skeleton LoadedComponents that the existing simulation creation accepts - the bridge that lets the dotnet sidecar run today's simulator on a design that arrived over the wire, as a BASELINE against which rewrites (and the Electron simulator itself) can be checked. Deliberately not part of the wire contract: the Simple types stay minimal, and a future .NET simulator consumes them directly with its own structures. This shim reconstructs only what the current pipeline demands - components with port lists (ports synthesized from the component type's arity; port ids never crossed the wire), connections resolved back from (component, port number) endpoints, and sheet IO signatures recomputed by the same function the loader uses. Geometry stays zeroed: the simulator never reads it.

SimpleJsonDotNet

Deserialises the JSON encoding used by Fable.SimpleJson, which is what the app writes into every .dgm file. Without this, nothing running under .NET can open a sheet, because Thoth.Json.Net - what `Helpers.jsonStringToState` otherwise uses there - cannot read what the app wrote. The two encodings disagree about unions (Thoth writes an array; SimpleJson writes a bare string for a nullary case and a single-property object otherwise), about options (Thoth wraps the value; SimpleJson writes the value itself, or null) and about maps with structural keys (SimpleJson writes an array of pairs). Rather than enumerate the `SavedInfo` schema - which would mean every `ComponentType` case, and a new one every time a component is added - this walks the target type by reflection, so it covers whatever the types say. Fable never sees any of this: under Fable the app calls SimpleJson itself, and Newtonsoft is not available. Under .NET it reaches this project transitively through Thoth.Json.Net.

SimTypes

SimulationGraphAnalyser

SimulationView

Simulator

StepPanelData

What the step simulator's panel shows, when the .NET sidecar is the one simulating. The same shape as the waveform viewer's cache and for the same reason: view code must read values synchronously, on every render, and a separate process cannot be asked synchronously. So the update function fills this and the view reads it (see the diagram at the top of `SimInterface.fs`). One snapshot, not a history. The panel shows one clock cycle - whichever the user has stepped to - so a fetch replaces what was here rather than adding to it, and everything in it is of one cycle of one session. That is what makes staleness impossible to show: a value is only ever read back for the cycle and epoch it was fetched for, and there is nothing else in here to read by mistake. A `ComponentId` IS an integer here - the whole design is reduced to integer ids when a project is opened (`Helpers.RegenerateIds`), which is what lets the sidecar name components at all.

StepSimulationTop

Symbol

SymbolHelpers

SymbolPortHelpers

SymbolReplaceHelpers

SymbolResizeHelpers

SymbolUpdate

SymbolView

SynchronousUtils

TestParser

TestWorker

TimeHelpers

TopMenuView

TruthTableCreate

TruthTableReduce

TruthTableTypes

TruthTableUpdate

TruthTableView

UIPopups

Update

UpdateHelpers

Verilog

VerilogAST

VerilogTypes

Version

WaveData

Where the waveform viewer gets its data, whichever simulator produced it. Two sources answer the same two questions. `Local` reads the renderer's own `FastSimulation` step arrays in place, which is what the viewer has always done. `Fetched` answers from the window most recently pulled off the .NET sidecar, which holds only the samples the current view draws - that being the whole point, since a sidecar simulation is sized for the machine's memory rather than a browser heap and its step arrays are far too large to ship. **Both questions are answered with a slice**, even the point reads: a cursor column is one sample of every selected wave, which is a window of one. That keeps one shape on the wire, one shape in the cache and one shape for the drawing code - and it is the shape that will carry run-length encoded waveforms when they arrive (see WaveSlice). **Why the source is module state.** The functions here are called from view code - `getWaveValue` while laying out the value column, the hover cache while building a tooltip - which has no model to thread a flag through, and reaching for a global is what that code already does for the simulation itself (`Simulator.simCache`). This mirrors it deliberately rather than inventing a second convention: set when the waveform simulator refreshes, cleared when it ends, and read from view code. See docs/mutableState.md - this is a cache of what the app is displaying, not model state.

WaveDrawn

What each waveform on screen is, and what it was drawn from. A drawn waveform is a pure function of the data for one signal over one window and a handful of display settings - so it does not belong in the model, and the model no longer holds it. What is kept here is the memo of that function: the SVG last made for each waveform, with the exact inputs it was made from. The view asks for the waveform it wants and gets it back without making it twice, and NOTHING here is consulted to decide what should happen next. Two things make the memo more than an optimisation. The first is the fallback. Where a signal's data has not arrived - the window moved and the answer is still on the wire - what is kept is what is on screen, and keeping it is better than emptying the row: waveforms a moment out of date are what a viewer over a wire looks like, a viewer that blanks itself on every scroll is what a broken one looks like. The stored Spec says which view the row is actually showing, which is how anything that has to agree with the screen - the hover tooltip - can be made to agree with it rather than with the controls. The second is that a render costs nothing when nothing changed. Issie renders the whole app on every message and does not memoise components, so this function is called for every waveform on every keystroke; without the memo each of those would rebuild a hundred SVGs. Module state, in the sense docs/mutableState.md allows: it is not model state written somewhere else, and no decision reads it. Discard it whenever the simulation it was drawn from is replaced - a driver index means something different in the next build, and a stale hit would draw the old design's signal under the new design's name.

WaveNames

The name a waveform is called, worked out from the component and port it is a wave of. Below the UI because both simulators produce it. A name carries the port's BIT WIDTH, and a width is a fact about the elaborated instance rather than about the sheet it instantiates - parameters see to that - so nothing above can work one out from a design. The alternative is the sidecar reimplementing which port of a Mux is called SEL and how an adder's carry in is written, and two answers that agree until they do not. Moved out of `WaveSimHelpers`, which is where it was and which now opens it. What stayed there is everything that needs a WaveSimModel: `makeWave` builds a Wave, and a Wave is a thing the selector holds.

WavePath

Naming a saved selection in a way that survives being saved. A `WaveIndexT` names a signal by component id and access path. Those are facts about one loaded design: ids are reallocated when a project is opened (`Helpers.remapLoadedComponent`), and a saved path's ids belong to OTHER sheets, which the loader's per-sheet mapping cannot see - so what it cannot place became the 0 sentinel (`Helpers.sheetOfJson`). A saved selection named that way was only as durable as the id scheme it was written under, which is why `3cpu/eep1`'s selection came back as component 0 and was dropped. What a person would use instead is what this uses: the LABELS of the custom component instances entered from the sheet being simulated, then the component's own label. That survives ids being reallocated, survives the file being written by a different version, and best-effort survives edits elsewhere in the design - which is what a saved selection is for, since it is a convenience rather than a record of anything. **A label path names exactly one wave-carrying component.** Labels are unique per sheet and enforced: `CanvasStateAnalyser.checkComponentNamesAreOk` refuses to simulate a sheet with duplicates. The types it exempts - `MergeWires`, `SplitWire`, `BusSelection`, `NotConnected` - carry no waveform at all, and the fifth, `IOLabel`, shares a label deliberately: a labelled net has one driver and every member reads it, so naming the group is naming the signal. Compared exactly, not case-insensitively. Issie's duplicate check groups on the label as written, so `abc` and `ABC` are two components and matching them loosely would make a path ambiguous where the design is not. Nothing here knows about a simulation: it walks the design's sheets, so it works before one is built - which is when the selection is loaded.

WaveProvider

Which simulator answers the waveform viewer, and - when it is the .NET sidecar - fetching what the view draws. The shape of it: the sidecar builds and runs the simulation, and the renderer asks it for one window - the samples the current view shows, for the waves it is showing - every time that view changes. `SidecarClient.simRead` takes exactly the viewer's own (StartCycle, SamplingZoom, ShownCycles) triple, so a view at any zoom is ONE request rather than one per wave, and the reply is read with no copy. What comes back becomes a `WaveData.Fetched` source, and the drawing code is none the wiser (see WaveSlice). Two requests, not one, and for a reason: the drawn window holds a sample every SamplingZoom cycles, but the cursor sits on an exact cycle which at any zoom above 1 falls between those samples. So the cursor column - every shown wave at that one cycle - is fetched alongside it. Both are sub-millisecond: the measured round trip is 0.3-0.6 ms and a typical window is a few hundred kB at 350-800 MB/s, against waveform generation that is budgeted at 50 ms a slice. **Not yet here**: buses wider than 32 bits, which `simRead` refuses, so their waves come back with no data and are drawn empty. That is the next thing to add, along with the RAM tables, which need a command of their own.

WaveSimHelpers

Miscellaneous helpers used tby waveform simulator

WaveSimHierarchy

The collapsed design hierarchy the wave selector shows. A design whose sheets instantiate one another multiplies out: seven sheets of a few instances each is tens of thousands of instances, and a selector with one entry per instance grows with that rather than with the design somebody wrote. So the hierarchy shown has one node per SHEET at each point in the tree, and the user says which instance of it they mean. This module works out, once per render, everything both panes of the selector need: which nodes there are, which instances each one could show, and which one it is showing. Both panes read the same answer, so they cannot disagree about it.

WaveSimNavigation

All the code that determines which clock cycles are viewed in the simulator. basic functions are zooming and panning (moving + or - in time). Additional function is a sample-based zoom for viewing very long waveforms.. Also implement cursor control.

WaveSimRams

RAM display in waveform simulator

WaveSimSelect

WaveSimSelectHelpers

WaveSimStyle

Functions to style the DOM elements used in waveform simulator

WaveSimSVGs

Functions to make SVGs of waveforms from FastSimulation data

WaveSimTop

Top-level functions for Waveform Simulator

WaveSimTypes

WaveSimWaveforms

showWaveforms and subfunctions to display in DOM the waveforms Waveform SVGs themselves are generated from simulation in WaveSimWaves module

WaveSlice

One waveform over one view window: what the waveform viewer asks for, draws from, and - when the simulator is the .NET sidecar - fetches across the wire. **Why a slice rather than an array.** The viewer never wants a whole waveform. It wants `ShownCycles` samples taken every `SamplingZoom` cycles from `StartCycle`, which is exactly what `SidecarClient.simRead` asks the sidecar for and exactly what the local step arrays can be read as. Making that window a value with a name gives the two simulators one shape to meet in: a slice from the local `FastSimulation` and a slice fetched from the sidecar are the same thing to everything downstream, so the drawing code neither knows nor cares which simulator ran. **Why the drawing code goes through the operations here and not the array.** What a waveform actually needs is its *transitions* - where the value changes - which is a run-length encoding of the samples in all but name. Today a slice holds dense samples and the operations below compute runs from them; the intended next step is for the sidecar to send runs, for a slice to hold runs, and for these same operations to return them with no work at all. That change should not reach the SVG code, so the SVG code must not index samples itself. Point access (a cursor value, a hover tooltip) is a handful of calls per render and can afford to go through a function; the drawing path asks for transitions once per waveform. **Reading costs nothing extra.** A slice does not copy: it names the array its samples already live in, where sample 0 starts, and how far apart samples are. Local step arrays are read with a stride of the sampling multiplier; a sidecar response is already sampled, so its stride is one and its base is the start of that signal's row in the reply.

WorkerInterface

Type something to start searching.