Known issues
What doesn't work yet, how to recognize it, and what to do about it.
This page is the open list — things that may still affect you. Fixed bugs live on closed GitHub issues; pull the latest main to pick them up.
If you hit something not listed here, please file an issue at simtenHQ/simten — concrete repros are gold.
Correctness
Elaboration edge cases surfaced by audit #140
A focused audit of elaboration.ts (after fixing #138) surfaced five narrow-but-real silent-correctness gaps. They are deferred to post-beta because they only bite specific unusual patterns; none affect typical circuits (gates, adders, registers, regular composites with internal nodes and gated outputs). Each is locked in by an it.fails(...) test in composite-patterns.test.ts that flips green when the fix lands.
Post-stitch multi-driver silently accepted (#143)
The multi-driver check runs per-circuit at circuit() definition time, not on the flattened netlist. If two primitives end up driving the same target through a feedthrough boundary, the conflict surfaces only after stitching and isn't caught.
How to recognize it: wrong outputs in a circuit that drives a composite's output port from outside the composite (e.g. inputs.B.to(p.y) where y is p's output). Workaround: don't drive a composite output port externally — the convention is that composite outputs are produced by the composite, not consumed as targets. The TS port-direction guard and the per-circuit multi-driver check together steer you away from this shape; the runtime gap only matters at API edge cases.
Width mismatches silently accepted at elaboration (#144, #145)
Elaboration never validates port-type or width compatibility between source and target. A bit source driving a bus(N) target (#144) or a bus(M) source driving a bus(N) target with M ≠ N (#145) passes silently; downstream evaluators coerce inconsistently.
How to recognize it: wrong values when a Constant({ value: 1 }) (default width 1) drives a wider bus, or a narrower bus drives a wider one. Workaround: the TypeScript port-direction types reject these mismatches at compile time in typical use — the runtime gap mostly bites hand-constructed IR or as any escapes. Use the width argument on Constant (e.g. Constant({ value: 1, width: 8 })) and prefer width-matching primitives so the source and target agree.
Deeply nested feedthrough composites produce a malformed netlist (#146)
At three or more levels of composites whose only output mechanism is a feedthrough (inputs.x.to(outputs.y), no gate at any level), elaboration produces a flat netlist where an internal primitive input port appears as a connection source and the top output has two drivers. Silent wrong values at simulation. Sibling class to #138 — depth-2 was fixed; depth ≥ 3 wasn't covered.
How to recognize it: wrong outputs in a circuit that stacks ≥ 3 levels of pure-feedthrough composites (board-of-board-of-board layering, repeated abstraction wrappers without internal logic). Workaround: include at least one gate (Not → Not, Buffer, anything non-trivial) at one of the inner levels, or flatten the wrapping to ≤ 2 nested levels. Gated nesting at any depth elaborates correctly.
Node-less composite with only a passthrough drops the signal (#147)
A composite with nodes: {} and only an inputs.x.to(outputs.y) connection — essentially a "labeled wire" composite — elaborates without error but the simulator returns 0 instead of the input value.
How to recognize it: a passthrough composite with no internal nodes produces 0 at its output regardless of input. Workaround: add at least one internal node to any composite, even a dummy gate wired off to one side. The Passthrough fixture in chained-passthrough.test.ts uses the canonical workaround pattern — inputs.x.to(d.in) against a Not whose output is unused.
Verilog export
Hierarchical mode not yet implemented
verilog_export currently emits a single flat module. The mode: 'hierarchical' option is accepted but not yet implemented (see packages/core/src/verilog/exporter.ts) — composite circuits are inlined into the top module rather than emitted as separate module declarations.
How to recognize it: the exported Verilog contains a single top-level module regardless of how deeply nested the source circuit is. Workaround: flat export is fully functional for synthesis (Yosys and nextpnr accept it); use it as-is. If you need hierarchical Verilog for readability or third-party tool consumption, post-process the output or open an issue.
VCD parsing
$dumpoff not handled per spec
The MCP VCD parser (packages/mcp/src/lib/vcd-parser.ts) does not yet drive all signals to x on $dumpoff. Most simulators we trace from (including our own) do not emit $dumpoff, so this is rarely observable, but VCD files produced by Icarus Verilog or commercial tools with $dumpoff directives will read back missing the x transitions.
How to recognize it: waveforms re-imported via read_waveform show the last-known value across a $dumpoff window instead of x. Workaround: none currently; file an issue if this affects your flow.
Sim-only primitives (not synthesizable)
Some stdlib primitives exist for visualisation/debugging and are not part of the Verilog export. They simulate fine, but they will be excluded from any synthesized output. They are marked meta: { synthesizable: false } in @simten/core/std:
Console— text output (display)HexDisplay,SevenSegment— numeric readoutsScreen,RasterDisplay— framebuffer surfacesProbe— debug observation
How to recognize it: the circuit simulates and renders, but verilog_export (or the FPGA build) either omits the node or errors. Workaround: wrap simulation-only display logic behind a top-level signal you can leave unwired on synthesis, or mark the wrapper circuit meta: { synthesizable: false } so it's expected.
Browser sandbox limits
The /circuit page (and the show_circuit MCP canvas) runs your code inside a cross-origin sandbox iframe (see Architecture). A few things follow from that:
npm imports require network (esm.sh)
import { x } from 'some-pkg' in a circuit is resolved at elaboration time via esm.sh. If you're offline, or esm.sh is unreachable, or the package isn't published in an esm-compatible shape, the binding will be undefined and you'll see x is not defined in the editor.
How to recognize it: ReferenceError on the imported binding. Workaround: check the package on esm.sh directly (e.g. https://esm.sh/<pkg>); use a known-good version pin ('pkg@4.3.2'); for Node-only packages, fall back to baking the data into a Constant/ROM outside the browser path.
Cross-file (multi-file) imports aren't resolved — only @simten/core/*
Circuit code submitted to the /circuit editor and show_circuit is executed by stripping its import statements and injecting the @simten/core stdlib into scope (browser: apps/sandbox/src/rewrite-imports.ts). So a circuit may import from @simten/core/* (resolved from injected scope) and — in the browser — bare npm specifiers (resolved via esm.sh), but it cannot import another local file (./helper.ts, ../shared.circuit.ts): the relative specifier passes through and resolves to nothing (no filesystem in the sandbox), so the binding is undefined. The host tools check_circuit and simulate_circuit share this limitation — they run the same import-stripping executor (executeCircuitCode in packages/core/src/circuit/execute.ts).
verify_circuit is the exception: it runs the file under real tsx with full Node module resolution, so multi-file testbenches (relative imports, workspace packages, npm) work there exactly like npm test.
How to recognize it: X is not defined / undefined for a binding imported from a relative path, in the /circuit editor or via check_circuit / simulate_circuit — while the same file runs fine under verify_circuit. Workaround: keep canvas/simulate circuits single-file, importing only from @simten/core/*; promote shared circuits into @simten/core/std so they resolve via injected scope. For genuinely multi-file work, use verify_circuit. A real fix would add a bundling/module-resolution step before execution — esbuild-bundling the entry plus its local files on the host, and a virtual-FS (or bundling) step in the sandbox — so /circuit and the host tools could accept multi-file circuits; tracked as a future enhancement.
Buffer is Node-only
A common gotcha when porting a verify testbench to a browser demo: Buffer.from([...]) works under tsx/node but is not defined in the browser sandbox. Use Uint8Array.of(...) / new Uint8Array([...]) instead — most npm crypto/hash packages accept it at runtime even when their TypeScript types only declare Buffer.
Worker compile timeout (5s)
Every circuit compile/elaborate runs inside a worker that the sandbox will kill after ~5 seconds. An infinite loop or runaway computation inside a connect callback (e.g. a for that builds an unbounded number of connections) will surface as a Worker restarted error rather than hanging the tab.
How to recognize it: the editor shows Worker restarted after a few seconds. Workaround: check the loops in your connect for bounds; remember connect runs at elaboration, so all its iteration must terminate.
postMessage … could not be cloned console warning
You may see a postMessage warning in the browser console — something the iframe sandbox tried to post wasn't structured-cloneable. It's benign: it appears on both the hosted /circuit page and the local MCP viewer (it isn't introduced by either), and it doesn't affect compilation, simulation, or rendering.
How to recognize it: a one-off … could not be cloned warning in DevTools, with no visible effect on the canvas. Workaround: none needed; safe to ignore. Tracked as low-priority.
Simulator limits
10 000-iteration propagation cap
Each set() / clock tick drives an event-driven propagation queue. After 10 000 node evaluations in a single propagation pass, the simulator throws:
Propagation did not stabilize after 10000 iterations. Possible unstable feedback loop in circuit.This is a safety net for combinational feedback loops with no register breaking them (an inverter whose output feeds its own input, etc.). It is not a depth/size limit — feed-forward circuits of any practical size settle well below the cap.
How to recognize it: the error message above. Workaround: find the combinational loop; break it with a Register or DFlipFlop.
get() only reads top-level output ports
simulate(C).get('foo') reads top-level outputs of C reliably. Reading an internal node's port ('subModule.out') returns 0 / uninitialised values regardless of the actual signal — it is not a supported observation API.
How to recognize it: an internal port reads 0 even when wiring and logic say otherwise. Workaround: expose the value you need as a top-level output of the circuit under test (wrap it in a probe harness with the signal piped out).