|
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
|
|