Issie logo Issie

RamStore Module

 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.

Types and nested modules

Type/Module Description

Constants

Addressing

How an address is turned into its slot number. This is the *only* place the strategies differ: everything after it works on slot numbers.

NodeOrLeaf

A 16-way trie over the address, used when the address space is too large to give every address a slot number in a flat array. It is grown in place and never copied: a slot is filled once and never repointed, which is what removes the per-write path copy the `Map` paid. A `Leaf` may sit above its natural depth - that path compression is the whole reason this is a union rather than a fixed-depth array of arrays, since depth follows from the address width and would otherwise be known - so the leaf carries its own address and it is checked on arrival.

Ram

A read/write memory as the simulator holds it.

Functions and values

Function or value Description

fixedOf mem

Full Usage: fixedOf mem

Parameters:
Returns: Ram

A read-only memory wrapped so that the one extraction path can serve ROMs and RAMs alike.

mem : Memory1
Returns: Ram

liveCountAt ram step

Full Usage: liveCountAt ram step

Parameters:
    ram : Ram
    step : int

Returns: int

How many words were non-zero at the end of `step`. Only diagnostics and tests want the exact number; the display wants `liveCountExceeds`.

ram : Ram
step : int
Returns: int

liveCountExceeds ram step limit

Full Usage: liveCountExceeds ram step limit

Parameters:
    ram : Ram
    step : int
    limit : int

Returns: bool

Whether more than `limit` words were non-zero at the end of `step`. This is all the RAM table needs: it chooses between listing every non-zero location and a windowed display, and the exact figure never appears. Asking the cheaper question means the walk stops as soon as the answer is settled, and usually it never starts - a memory that has never had more than `limit` addresses written cannot have more than `limit` non-zero.

ram : Ram
step : int
limit : int
Returns: bool

ofMemory window mem

Full Usage: ofMemory window mem

Parameters:
Returns: Ram

Build the store for one read/write memory. `window` is the simulation's `MaxArraySize`: the number of past steps that can still be asked about.

window : int
mem : Memory1
Returns: Ram

readAddrBigIntDataBigInt ram addr

Full Usage: readAddrBigIntDataBigInt ram addr

Parameters:
    ram : Ram
    addr : bigint

Returns: bigint
ram : Ram
addr : bigint
Returns: bigint

readAddrBigIntDataUInt32 ram addr

Full Usage: readAddrBigIntDataUInt32 ram addr

Parameters:
    ram : Ram
    addr : bigint

Returns: uint32
ram : Ram
addr : bigint
Returns: uint32

readAddrUInt32DataBigInt ram addr

Full Usage: readAddrUInt32DataBigInt ram addr

Parameters:
    ram : Ram
    addr : uint32

Returns: bigint
ram : Ram
addr : uint32
Returns: bigint

readAddrUInt32DataUInt32 ram addr

Full Usage: readAddrUInt32DataUInt32 ram addr

Parameters:
    ram : Ram
    addr : uint32

Returns: uint32

The four combinations the evaluators need, named as the `Memory1` versions they replace so that the call sites read the same.

ram : Ram
addr : uint32
Returns: uint32

readBigVal ram addr

Full Usage: readBigVal ram addr

Parameters:
    ram : Ram
    addr : uint32

Returns: bigint

Current contents at `addr`, on the bigint-value path. A store keeps values in one of two forms, chosen once by word width, and the caller has to ask for the one it holds - the evaluators do, via `BigIntState`. Asking for the other would otherwise read an array that was never filled, so it is redirected rather than trusted.

ram : Ram
addr : uint32
Returns: bigint

readU ram addr

Full Usage: readU ram addr

Parameters:
    ram : Ram
    addr : uint32

Returns: uint32

Current contents at `addr`, on the uint32 path. One array index in the dense case, which is the case every RAM a design can actually simulate falls into.

ram : Ram
addr : uint32
Returns: uint32

reset ram

Full Usage: reset ram

Parameters:

Put the memory back to the contents it was built with, discarding all history. Called when a simulation restarts, which is the one thing the old representation got wrong: it read the state left by the wrapped-out end of the previous run instead.

ram : Ram

sparseUpTo ram step limit

Full Usage: sparseUpTo ram step limit

Parameters:
    ram : Ram
    step : int
    limit : int

Returns: (bigint * bigint) list option

The non-zero locations at the end of `step`, in address order - or None when there are more than `limit` of them. This is what a RAM table's sparse display is: every location worth a row, when there are few enough to list. It replaces asking `liveCountExceeds` and then building the answer with `toMemory`, which is the right question answered the expensive way - the count is bounded by the limit but the whole-memory read is not, so a memory with 65,536 addresses written and fifty non-zero now took the sparse branch and paid 80 ms for it, on every render. Bounded by TWO things, and it needs both. The slot table is the set of addresses that could be non-zero - a slot exists once an address has been written, and `reset` seeds the initial contents as writes at step -1 - so the walk stops at the first `limit + 1` non-zero words. But a memory with 65,536 addresses written and fifty non-zero now would run out of SLOTS before it ran out of limit, and walking all of them is the 80 ms read this exists to avoid. So a memory with more than `Constants.maxSlotsForWholeRead` addresses ever written answers None without walking at all: the caller shows a window instead. The count that decides it is therefore addresses EVER WRITTEN, not words currently non-zero. It has to be: the second is what the walk is for, so it cannot also be its precondition.

ram : Ram
step : int
limit : int
Returns: (bigint * bigint) list option

toMemory ram step

Full Usage: toMemory ram step

Parameters:
    ram : Ram
    step : int

Returns: Memory1

The whole memory as of `step`, as the immutable type the rest of Issie uses. Only for the places that genuinely want all of it - a memory diff, a golden file - never per render.

ram : Ram
step : int
Returns: Memory1

toMemoryIfSmall ram step

Full Usage: toMemoryIfSmall ram step

Parameters:
    ram : Ram
    step : int

Returns: Memory1 option

The whole memory as of `step`, when reading it is affordable - None when it is not. What "affordable" means, and why it is a count of addresses ever written rather than of words held, is on `Constants.maxSlotsForWholeRead`. A caller told None must show something bounded instead, and must not fall back to `toMemory`.

ram : Ram
step : int
Returns: Memory1 option

wordAt ram step addr

Full Usage: wordAt ram step addr

Parameters:
    ram : Ram
    step : int
    addr : bigint

Returns: bigint

What one address held at the end of `step`.

ram : Ram
step : int
addr : bigint
Returns: bigint

writeAddrBigIntDataBigInt ram step addr value

Full Usage: writeAddrBigIntDataBigInt ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : bigint
    value : bigint

ram : Ram
step : int
addr : bigint
value : bigint

writeAddrBigIntDataUInt32 ram step addr value

Full Usage: writeAddrBigIntDataUInt32 ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : bigint
    value : uint32

ram : Ram
step : int
addr : bigint
value : uint32

writeAddrUInt32DataBigInt ram step addr value

Full Usage: writeAddrUInt32DataBigInt ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : uint32
    value : bigint

ram : Ram
step : int
addr : uint32
value : bigint

writeAddrUInt32DataUInt32 ram step addr value

Full Usage: writeAddrUInt32DataUInt32 ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : uint32
    value : uint32

ram : Ram
step : int
addr : uint32
value : uint32

writeBigVal ram step addr value

Full Usage: writeBigVal ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : uint32
    value : bigint

ram : Ram
step : int
addr : uint32
value : bigint

writeU ram step addr value

Full Usage: writeU ram step addr value

Parameters:
    ram : Ram
    step : int
    addr : uint32
    value : uint32

Record a write, unless it changes nothing. A write of the value already at that address is not a write: skipping it costs one comparison on a value the reducer has already read (it is the old contents, which a RAM returns as its output) and saves an entry, which every later search and every later compaction then does not have to walk. Nothing observable changes - the contents are identical by definition, and the RAM table's read/write highlighting comes from the input step arrays rather than from here.

ram : Ram
step : int
addr : uint32
value : uint32

Type something to start searching.