Repository navigation
Releases: logigator/logigator-sim
Releases · logigator/logigator-sim
Release list
v0.7.0
Breaking
getStatus()returns{ running, tick, linkCount, componentCount }.runningistruefrom
therunAsync()call until its promise settles. It replacesstate, and theSimStatetype and
Node enum are gone.speedis gone too: for a ticks/s readout, sampletickagainst a clock.
During a run it is at most one batch behind: about 1 ms on Node, 5 ms in WASM.linkCount()andcomponentCount()are gone from both bindings: read them from
getStatus().- On Node, a delta snapshot's
idsis aUint32Array, like itsinvalid, instead of a
Bufferof little-endianu32s. - In WASM,
runAsync()rejects while another run on the same simulation is in progress and not
stopped, andrun()throws then, as on Node. Both used to run, interleaved between batches. - Both bindings read boards and run configs with the same rules and messages:
- A field that is
undefinedis absent, andnullis refused. WASM used to read anull
field or bound as absent. - Numbers must be exact non-negative integers where the board holds a
u32or a byte. Node used
to truncate them, so atypeof 1.5 built a NOT and aperiodof 2.7 became 2. - Unknown fields are ignored.
- A run config must be an object. Node used to start an unbounded run for
runAsync(5). A
malformed config throws fromrunAsync()in WASM too, instead of rejecting its promise. - Fields are checked in order, component by component, so a board with several mistakes names
the same one in both.
- A field that is
- In WASM,
link(id)throws for an id past the last link, as on Node. It used to returnfalse. - In Rust:
Simulation::runreturns nothing, andSimState,Simulation::state,Simulation::stopand
thestateandspeedfields ofStatusare gone.read_state(comp, offset, len)returns the bytes, andread_records(comp, since, max)returns
them inRecordswith their ticks, instead of filling buffers;SimError::RecordTicksis
gone.InputRequestbuilds an ownedsubmit_inputrequest;inputs()lists theUserInputs.Board,Simulation::newandStateLayout::ofare no longer public: build a simulation
withSimulation::from_descriptor, and read a layout withSimulation::state_layout.SnapshotInfodropschanged: a delta's changed links aresnapshot_ids().
Performance
- In browsers, the WASM
runAsync()no longer idles 4 ms between batches: it yields through a
MessageChannelinstead of a nestedsetTimeout(0), which browsers clamp to 4 ms. An unbounded
run now ticks as fast as a blockingrun(), about 1.5× its previous speed on a board that
changes every tick. Under Node it yields withsetImmediate, withoutsetTimeout's 1 ms.
Fixed
- A run with an
msbound ends within about a tick of it on any board, in the CLI and both
bindings. The engine used to read the clock only after its first 1 021 ticks, and the Node addon
every 4 093, so on a board at 5 000 ticks/s,run({ ms: 5 })took 0.2 s in WASM and 0.6 s on
Node. runAsync()works in batches of about 5 ms in WASM and 1 ms on Node on any board, sostop(),
snapshots and messages wait at most that long. On a board at 5 000 ticks/s, a WASM batch used
to hold the thread for 0.2 s and a Node snapshot waited up to 0.9 s. On Node,stop()now takes
effect at the end of a batch rather than at the next tick. Batch lengths vary by a tick, so state
read between batches reaches every phase of a periodic signal, even on a board slow enough for a
batch to be a few ticks.- During a WASM
runAsync(),getStatus()reports the run, as on Node. It used to report
Stopped. On Node, it reports the run from therunAsync()call until the promise settles; it
used to report it only once the worker had picked the run up, and could stop before the promise
settled. readRecordsallocates for the records it returns. It used to allocate the recorder's full
capacity on every call, about 12 MiB for a large recorder with ticks; in WASM that could grow
memory and detach every outstanding snapshot view.- Freeing a WASM simulation (
destroy(),free()or garbage collection) during arunAsync()
ends the run. It used to keep ticking forever, and kept a Node process alive. - A
runAsync()orrun()called straight afterstop(), before the stopped run's promise has
settled, starts. In WASM, the newrunAsync()used to revive the stopped run, which then never
ended. On Node, it was rarely rejected as a run already in progress.
Full Changelog: v0.6.0...v0.7.0
v0.6.0
Breaking
- The board format names its parameters and drops its link count:
- A board is
{ components };linksis gone, and the links are numbered from 0 to the highest
id a pin names. A component may leave out an emptyinputsoroutputs. opsis replaced by named parameters: a CLK'speriodandmode, a RAM'sread, a ROM's
data, a recorder'scapacityandrecordTicks, and an addressable register'sdataBus. A
parameter on any other type, or a missingperiod,capacityordataBus, is refused as
BadParam { idx, ty, param, reason }.- A ROM's
datais bytes (aUint8Arrayor an array of 0–255 in JS, an array in JSON). Short
data is still zero-filled; data longer than the ROM's table is refused instead of cut off. - The CLI's JSON boards refuse unknown fields, such as a leftover
linksorops. The JS
bindings ignore them, so a board that still passesopsis refused for a CLK, recorder or
addressable register but silently gets an all-zero ROM and a RAM in its default mode. - A link id must be below 2³⁰ (
LinkIdOverflow);LinkOutOfRangeandLinkCountOverfloware
gone, andBadArityno longer carries anopscount. .lgbis version 3: no link count in the header, and the parameters follow each component's
pins, flagged in the reservedu16after its type id.- In Rust, the parameters are optional fields of
ComponentDescriptor(with theClkModeand
RamReadenums;record_ticksreplacesRECORD_TICKS), set with struct-update syntax over
ComponentDescriptor::new(ty, inputs, outputs).BoardBuilder::new()takes no link count,
component()noops, andadd()appends aComponentDescriptor.
- A board is
- A CLK is a square wave by default,
periodticks low thenperiodhigh; the old one-tick pulse
afterperiodticks low ismode: "pulse". A period of 1 is unchanged. A halted CLK restarts
its cycle once its halt input goes low, instead of resuming its count where the halt caught it. - A RAM's outputs follow its address: they show the addressed word one tick after the address or
the memory (including throughwriteState) changes, with no clock. Writes still happen on a
rising clock. The old outputs, which change only on a rising clock, areread: "clocked". - A MUX takes no parameter: its select width is the one
swiths + 2ˢinputs. A DEMUX takes
its select bits first, as a MUX does:[s0, …, s(n-1), data]instead of[data, s0, …]. - The LED matrix (type 204) is a generic addressable register whose outputs are its stored bits
(CompType::AddressableRegisterin Rust, formerlyLedMatrix). ItsdataBusis its data bus
width, from 1 to its output count, instead of a size where anything above 4 meant 8. Its outputs
form words of that width, the last possibly partial; its address bus is
⌈log₂(⌈outputs / dataBus⌉)⌉ bits, so every output can be written, and its input count must be
exactly address + data + clock. The editor's 4×4, 8×8 and 16×16 matrices keep their pins with
dataBus4, 8 and 8. - Node.js 22 or newer is required; Node.js 20 is end-of-life.
- Building from source needs Rust 1.88 or newer.
Added
- Invalid links: a link is invalid while its drivers disagree, or while an SR flip-flop clocked
with S and R both high drives it (Q set, Q̄ low, until a clock with only S or only R). It keeps
its wired-OR value and nothing downstream is flagged. Snapshots list them:snap.invalidon
Node,invalidPtr/invalidLen(bytes ofu32ids) on WASM, andsnapshot_invalid(),
link_invalid(id)andinvalid_links()in Rust;sim traceframes carry a non-emptyinvalid
list. - An encoder can have a valid output after its number bits, high whenever any input is, so input 0
alone differs from no input:2ⁿinputs takenoutputs, orn + 1with the valid pin.
Performance
- Ticks are 46–66% faster on every benchmark board (57% on the real-world CPU board), and a board
with only clocks running ticks more than twice as fast. A tick visits only the component types
it has work for, finds a link's consumers with one lookup, and skips bounds checks on link ids,
which board compile already keeps in range. NOT, DELAY, two-input AND/OR/XOR, the adders and the
D flip-flop read their wiring from one record each, and a link with a single driver flips without
counting its drivers. Out of range, WASMlink(id)now readsfalseinstead of trapping.
Fixed
- An addressable register whose word count is not a power of two no longer writes past its own
outputs when addressed beyond its last word, which drove the next component's output pins or
crashed the simulation (a trap in WASM, a thrown error in Node). Such a write now lands only on
the outputs that exist. - A CLK whose period is above 2³¹ no longer fires every tick.
- A link driven by more than 65 535 outputs is refused as
TooManyDrivers { link, count, max }.
Its count of high drivers used to wrap, so with every driver high it could read low. - On Node, a
stop()made right afterrunAsync(), before the worker picked the run up, is no
longer lost. The run used to go on forever, and its promise kept Node from exiting.
Full Changelog: v0.5.0...v0.6.0
v0.5.0
Breaking
triggerInput/trigger_inputandInputEventare removed, in Rust, Node and WASM. A
UserInputis now driven by a queue of timed frames:submitInput(writes)queues them and
inputProgress(out)reports how many event frames are still pending per input. The old calls
map onto frames asCont(s)→{ comp, frames: [{ state: s, coalesce: true }] }and
Pulse(s)→{ comp, frames: [{ state: s }, { state: <all false> }] }; a pulse that should
leave an input's other pins alone gives themnull.- An input write reaches its wires one tick after it is submitted: one tick sooner than before, and
the same on a wire shared with another driver, where it used to arrive a tick early or hide a
one-tick pulse. An input's pins and wires now change in the same tick. - A frame's
statemust have exactly one entry per output pin; the old call padded a short array
withfalse. - CLI fixtures'
triggersfield is no longer read; timed input isinputWritesonly (see
Added). A trigger at tick T becomes a request at tick T + 1 (cont→ one frame,pulse→ the
set frame then an all-false frame), which keeps the links of its trace unchanged. sim verify, which ran the oldtest.jsfixture format, is removed.- In
contract.d.ts,SimulationandSimulationConstructortake a second type parameter,
Async:truefor the Node binding, whose state reads return promises,falsefor WASM. Simulation.fromJsonis removed from the Node and WASM bindings and fromcontract.d.ts; use
new Simulation(JSON.parse(json)).- A board is validated once, when it is compiled, whichever way it comes in, so a malformed board
gets the same error in the CLI, Node and WASM. An unknown type is reported as
UnknownComponentType { idx, ty }, naming the component, and a type id or negated pin index
whose low 16 bits name a real type or pin is rejected instead of narrowed onto it. - In Rust,
ComponentDescriptorholds plain numbers:tyis theu32type id and
neg_inputs/neg_outputsareVec<u32>;BoardBuilder::component_negtakes&[u32].
CompType::id()returnsu32,CompType::try_from_u16is nowCompType::from_id(u32), and
CompTypeno longer implements serde orTryFrom<u16>.NegateOutOfRange::pinis au32,
decode_boardno longer checks type ids, andencode_boardreturns aResult, refusing ids
too wide for the format.
Added
- Input frames carry a
hold(minimum ticks on the pins before the next frame), anullper pin
to keep that pin's value, and acoalesceflag for levels: coalescing frames submitted between
two ticks merge per pin and reach the circuit once.replace: truedrops an input's pending
frames and ends its running hold. Sequencing happens inside the tick loop, so the trace no longer
depends on batch sizes or host timing. - Each input holds at most 65 536 pending event frames; a write over the limit is refused whole.
- On Node,
submitInputvalidates on the JS thread and returns without waiting for the worker. - CLI fixtures accept an
inputWritesfield,[{ tick, writes }], holdingsubmitInputrequests
in the JS shape;run,traceandbenchsubmit each once the simulation has runtick
ticks. - Component state, in Rust, Node and WASM:
stateVersionsfills a caller-owned buffer with a
version per component that changes whenever its state does,readState/writeStateread and
write a RAM's memory (bit-packed words, the layout of a ROM'sops), andreadRecordsreads a
recorder's records together with its count. Reads are coherent at a tick boundary; on Node
readStateandreadRecordsreturn promises, andwriteStateis checked on the JS thread and
queued in order withsubmitInput. - The recorder (type 201) captures a data bus of up to 32 bits on each rising trigger edge into a
ring of its lastcapacityrecords, optionally with the tick of each. It is how a terminal gets
every character its circuit writes, one per tick if need be. - New errors for the state API:
ComponentOutOfRange,StateRange,StateReadOnly,
NotARecorder,RecordsAheadandRecordTicks.
Changed
- On WASM, a malformed descriptor or run config is reported with the field at fault, such as
components[3].inputs must be an array of u32. - The README is rewritten as a shorter guide to what the engine is and how to use it, with a
component table listing each type's inputs, outputs,opsand behavior; the full API types are
in@logigator/sim/contract. It corrects the CLK'sops[0], which is the number of ticks the
clock stays low before a one-tick high, not ticks per half-cycle, and the WASM snapshot's field
names (isDelta,valuesPtr,valuesLen).
Performance
- The WASM build is about half the size:
sim_wasm_bg.wasmis 108 KB instead of 225 KB (45 KB
instead of 95 KB gzipped), as it no longer carries serde, a JSON parser or a stable sort. new Simulation(descriptor)on WASM builds a large board 2–3× faster (a 200k-component board in
~30 ms instead of ~80 ms).
Fixed
run()with anmstoo large to represent, such asInfinity, no longer crashes (a trap in
WASM, a thrown panic in Node); an out-of-rangemsortickscounts as no bound, sorun()
refuses it as unbounded andrunAsync()runs untilstop().sim bench --msrefuses it too.
Full Changelog: v0.4.0...v0.5.0
v0.4.0
Breaking
- The negated-pin descriptor fields are renamed from
negatedInputs/negatedOutputsto
negInputs/negOutputs, in JSON boards and in the Node and WASM bindings. The.lgbbinary
format is unchanged. UserInputnow maps only from component type id200. Ids201–299used to alias it and are
now rejected as unknown component types.- Boards whose
link_countexceeds 2^31 are rejected withLinkCountOverflow, because the top bit
of a link id now carries the negation flag.
Changed
sim benchis time-bound by default: each repeat runs for--msmilliseconds (default 1000) and
reports the ticks that landed. Pass--ticks Nfor the previous fixed-tick mode.
Performance
- Pin negation is packed into the top bit of the link ids, so reading or driving a pin no longer
loads a separate negation bitset. This recovers most of the throughput 0.3.0 lost: on the
real-world CPU benchmark board the cost drops from 8.4% to 3.2%.
v0.3.0
Breaking
- The
.lgbbinary board format is now version 2, which stores per-pin negation. Version 1 files
are rejected withunsupported version; re-export them from JSON. JSON boards without the new
fields load unchanged.
Added
- Per-pin negation:
negatedInputs/negatedOutputson a component descriptor list the pin indices
to invert, in JSON boards and in the Node and WASM bindings. - A negated clock or enable pin inverts its trigger sense: a negated clock makes the component
falling-edge triggered, with no spurious edge at power-on.
Performance
- Negation is always applied, even on boards that don't use it, which costs some throughput (about
9% on the real-world CPU benchmark board).
v0.2.0
Breaking
- The WASM binding's
getStatus()fields are camelCased to match the Node binding:
link_count→linkCount,component_count→componentCount. - The WASM
SnapshotViewfields are camelCased:is_delta→isDelta,values_ptr→
valuesPtr,values_len→valuesLen.
Added
@logigator/sim/contract: a type-only subpath with the public interface that both bindings
implement.- The Node binding exports the
SimStateandInputEventenums.getStatus().stateand
triggerInput()'s event argument use them (the numeric values are unchanged), andrunAsync()
andsnapshot()now have precisePromisereturn types.
Fixed
- Time-bounded runs land close to their deadline instead of overshooting by up to a full batch.
- Periodic sampling during runs no longer aliases short-period signals: the state seen at the end of
a run, such as a NOT gate feeding itself, no longer always lands on the same phase.
v0.1.3
Changed
- WASM
runAsync()runs in fixed wall-clock slices instead of fixed 4096-tick batches, so frame
pacing andstop()latency stay constant regardless of board size, and the speed readout stays
live during the run.
v0.1.2
Fixed
@logigator/sim/wasm/sim_wasm_bg.wasmis in the package exports, so the.wasmfile can be
imported directly.
v0.1.1
Fixed
- The published package exposes the
logigator-simcommand; 0.1.0 shipped the launcher without the
binentry. - The published package includes the README and LICENSE.
v0.1.0
Initial release of the Rust simulation engine.
Added
- A change-driven tick loop with the full Logigator component set, verified tick-exact against the
previous C++ engine with two deliberate exceptions: the RNG component is seeded per component
(reproducible and independent of evaluation order), and the SR flip-flop is rising-edge triggered
like the D and JK flip-flops. @logigator/sim/node: a native Node.js addon with synchronous runs,runAsync()on a background
worker thread, and coherent full or delta snapshots while a run is in progress.@logigator/sim/wasm: a WebAssembly build (web target, SIMD128) with zero-copy state views and a
runAsync()that yields to the event loop between batches.- The
simCLI (run,trace,verify,bench), installed aslogigator-simthrough npm and
attached to the GitHub release as standalone binaries. - JSON and
.lgbbinary board formats. - Prebuilt binaries for Linux x64/arm64 (glibc 2.17+), Linux x64 musl, macOS x64/arm64, and Windows
x64.