Clash Toolkit
Community extension — maintained by Lucas Bollen (QBayLogic). Not an official release of the Clash project.
Synthesize Verilog from Haskell functions using Clash, explore the result with Yosys, and place-and-route for ECP5, iCE40, and Gowin FPGAs with nextpnr — all from inside VS Code.
The extension integrates with Haskell Language Server (HLS) to find functions in your Clash project, determines which ones are monomorphic (and therefore synthesisable), then drives the full hardware workflow:
Haskell source → Clash (Verilog) → Yosys (synthesis) → nextpnr (place & route)
At every stage you can inspect output, view statistics, and open a schematic diagram rendered with netlistsvg.
Feature Highlights
- Function detection via HLS — finds monomorphic functions automatically
- Code actions — press
Ctrl+.on a function to elaborate, synthesize, or place & route it directly - Managed toolchain — Yosys and nextpnr can be downloaded on demand instead of installed by hand
- Sidebar — browse functions, inspect synthesis results, and revisit past runs from the Clash Synthesis view
- Editable synthesis scripts — override the Yosys script per target from the settings panel
- Optional out-of-context synthesis — synthesize each component standalone (generic cells, no technology mapping) for a per-component diagram and statistics
- Timing targets from the design — the top entity’s clock domain sets the frequency place & route is judged against
- Schematic diagrams — netlistsvg-rendered SVG schematic per synthesis run, with no extra tool to install
- Hierarchical inspection — expand a module in the sidebar to reach the components it instantiates, each with its own diagram
- Full PnR flow — ECP5 / iCE40 / Gowin place & route with timing analysis and utilization reports
- Debug logging — all tool invocations logged to
.clash/debug.log
Getting Started
Prerequisites
You provide: the Haskell toolchain
These must be available in your environment (e.g. via nix develop, ghcup, or
your system package manager). The extension cannot install them for you.
| Tool | Purpose |
|---|---|
Cabal (drives cabal run clash-synth:clash --) | Builds your project and invokes Clash to generate Verilog |
| Haskell Language Server | Function detection and type information |
The extension provides: the EDA tools
Yosys and the nextpnr-* binaries do not have to be on your PATH. When a
command needs one that is missing, the extension offers to download a
self-contained OSS CAD Suite
build into its own private storage and use it from there.
| Tool | Purpose | Needed for |
|---|---|---|
| Yosys | Logic synthesis and statistics | Elaborate, Synthesize, Place & Route |
| nextpnr-ecp5 | Place & route for Lattice ECP5 | Place & Route, ecp5 target |
| nextpnr-ice40 | Place & route for Lattice iCE40 | Place & Route, ice40 target |
| nextpnr-himbaechel | Place & route for Gowin | Place & Route, gowin target |
Anything already on your PATH is used as-is — a managed download is only ever
offered for tools that are missing, and only for the ones you tick in the
prompt. Run Clash: Check Toolchain to probe cabal, Yosys, and
nextpnr-ecp5, or Clash: Install Toolchain to manage the download
explicitly. The iCE40 and Gowin binaries are checked when you actually run
Place & Route for those targets.
The suite is a single archive (335–730 MB depending on platform) pinned to one release, so the first download takes a while regardless of how many tools you select.
Schematic diagrams need no tool at all: they are rendered by netlistsvg, which is bundled with the extension. See Circuit Diagrams.
See Managed Toolchain for the full details.
Quick Start
- Open a Clash project in VS Code (one that builds with
cabal build). - Make sure HLS is running (install the Haskell extension).
- Open a
.hsfile containing monomorphic functions. - Either:
- Run Clash: Detect Functions from the command palette and pick a function, or
- Place your cursor on a monomorphic function and press
Ctrl+.to use a code action.
How Synthesis Works
-
Function detection — HLS provides document symbols and hover types. The type analyser checks whether a function is monomorphic (all concrete types, no type variables).
-
Wrapper generation — For a function like
topEntityin moduleExample.Project, the extension generates a wrapper module under.clash/synth-project/src/:{-# OPTIONS_GHC -Wno-orphans #-} module ClashSynth_TopEntity where import Clash.Prelude import qualified Example.Project topEntity = Example.Project.topEntity {-# ANN topEntity (Synthesize { t_name = "top_entity" , t_inputs = [ PortName "CLK" , PortName "RST" , PortName "EN" , PortName "IND" ] , t_output = PortName "OUT" }) #-} {-# OPAQUE topEntity #-}Compound types like
DiffClockare handled automatically withPortProductannotations. -
Synthesis cabal project — The extension maintains a cabal project at
.clash/synth-project/that depends on your package. This lets Clash resolve all transitive dependencies correctly. -
Clash compilation — Runs
cabal run clash-synth:clash -- ClashSynth_TopEntity --veriloginside the synth project. -
Yosys synthesis — Runs Yosys with a script chosen by the
synthesisTargetsetting (generic,ice40,ecp5,xilinx,gowin,quicklogic,sf2). Each target’s script is editable — see Configuration. For multi-component designs with out-of-context mode enabled, each component is synthesized standalone with a fixed generic script instead — see Configuration. -
Place & route — Runs nextpnr for the selected device and reports timing and utilisation. The target frequency comes from the top entity’s clock domain in the Clash manifest.
Managed Toolchain
Yosys and the nextpnr-* binaries do not have to be installed by hand. When a command needs one that is missing, the extension offers to download
a self-contained OSS CAD Suite
build into its own private storage and run it from there.
This applies only to the EDA tools. Cabal and HLS are still yours to provide — see Getting Started.
PATH first
Nothing is downloaded behind your back, and a managed install never shadows your own tools. For every tool the extension spawns:
- If you opted to have that tool managed and a managed copy is on disk, the managed absolute path is used.
- Otherwise the bare command name is used, so your normal PATH lookup applies.
So an existing Yosys keeps being used, and the download offer only ever appears for tools that could not be found.
Choosing what to manage
Both the automatic prompt and Clash: Install Toolchain show the same checklist:
| Tool | Used for |
|---|---|
| Yosys | Elaborate, Synthesize, Place & Route |
| nextpnr-ecp5 | Place & route for Lattice ECP5 |
| nextpnr-ice40 | Place & route for Lattice iCE40 |
| nextpnr-himbaechel | Place & route for Gowin |
Each row is annotated with its current state — found on PATH, managed, or not found — download. Missing tools are pre-checked and tools already on your PATH are not, so accepting the default selection fills exactly the gaps. An existing opt-in is preserved, so reopening the prompt will not silently un-manage anything.
Ticking a tool records the opt-in and, if a managed binary for it is not already present, fetches the archive. Dismissing the prompt changes nothing.
Because the suite is distributed as one archive containing every tool, the download size does not depend on how many boxes you tick — selecting only Yosys costs the same as selecting everything:
| Platform | Approximate download |
|---|---|
| Linux x64 | 730 MB |
| Linux arm64 | 620 MB |
| macOS arm64 | 510 MB |
| macOS x64 | 485 MB |
| Windows x64 | 335 MB |
On any other platform or architecture, automatic installation is unavailable and the extension says so rather than failing obscurely — install the tools yourself and put them on your PATH.
Where things land
The suite is extracted into the extension’s global storage directory, in an
oss-cad-suite/ subtree with the binaries under bin/. Nothing is written to
your workspace, and nothing outside that directory is touched. Alongside the
install, a marker file records which release is present.
When a managed binary runs, its environment is augmented so it can find its co-located siblings and shared libraries — that is why using the suite’s Yosys with a system nextpnr is not something you need to think about.
Pinned releases
The extension pins one OSS CAD Suite release rather than tracking the latest, so every user gets the same mutually-compatible set of tools. Because a new pin means a fresh multi-hundred-megabyte download for everyone, the pin is bumped deliberately, not incidentally.
Concurrent misses share a single download, so two commands that both discover a missing tool will not fetch the archive twice.
Checking what resolved
Clash: Check Toolchain probes cabal, Yosys, and nextpnr-ecp5, and reports
what is reachable. The Tools section of the
Settings Panel shows the same information continuously —
including the resolved path, which is the quickest way to tell whether a given
tool is coming from your PATH or from the managed install.
nextpnr-ice40 and nextpnr-himbaechel are not part of that upfront probe.
They are checked when Place & Route runs for their target, which is also when
the download prompt would appear for them.
The Sidebar
The extension contributes a Clash Synthesis view to the activity bar: one tree with three sections, covering the whole workflow — pick a function, inspect what synthesis produced, and go back to earlier runs.
Clash Synthesis
FUNCTIONS ← what you can synthesize
…
RESULTS ← what the last run produced
…
HISTORY ← every previous run, on disk
…
Each section header carries a status of its own: which file’s functions are listed, which run is loaded into Results, or why HLS has nothing to say. Hover a header for the long form. Sections collapse independently and stay that way.
Headers are upper-cased and a blank row precedes each one, so a section reads as a boundary rather than as one more row among its own contents. A tree has no separator API, so those blank rows are real rows — inert ones, with nothing to expand and no context menu, announced to screen readers as separators.
One view means one title bar. It holds the four main-flow actions — Generate Verilog, Elaborate, Synthesize, Place & Route — plus Refresh and Open Settings. Refresh re-reads everything that comes from outside the extension: the active file’s functions and the runs on disk.
Most of the extension’s commands live here rather than in the command palette, because they act on a specific tree item. See Commands for the full list.
Functions
Shows the functions in the currently active Haskell file, split into two expandable sections with counts:
- Monomorphic (n) — fully concrete types, so they can be synthesized
- Polymorphic (n) — greyed out; these cannot be synthesized directly
Each entry shows the function’s type signature as its description, and hovering gives the signature formatted as Haskell plus a note on why it can or cannot be synthesized. Clicking a function jumps to its definition.
When the list is empty
An empty list has several possible causes, so the view names the one that applies instead of showing nothing:
| Row | Meaning | What to do |
|---|---|---|
| Analyzing… | The extension is analysing symbols HLS returned | Wait; it’s quick |
| No symbols from HLS yet | HLS is reachable but returned nothing for this file | Usually it is still loading the project. The view re-checks when HLS next reports on the file; Refresh re-checks now |
| HLS unavailable — Haskell extension not installed | Function detection needs haskell.haskell | Click the row to open it in the Marketplace |
| HLS unavailable — Haskell extension did not start | It is installed but did not activate | Click the row to retry; its own output channel says why |
| Open a Haskell file to see functions | No Haskell file is active | Open one |
| Monomorphic (0) / Polymorphic (0) | HLS answered, and this file defines no top-level functions | Nothing — this is a real answer |
The distinction between the last two matters: “no symbols yet” is not a verdict. While the Haskell Language Server loads a project it answers with nothing, which looks exactly like a file with no functions, so the view refuses to claim either.
The extension starts the Haskell extension itself if it is installed but idle, and re-checks automatically when HLS publishes diagnostics for the file it is showing — that is the only signal another extension can observe, since HLS exposes no readiness API. If a file’s functions still don’t appear after HLS has settled, use Refresh in the title bar.
Results
Populated by the most recent Elaborate, Synthesize, or Place & Route run.
Each synthesized module appears as a row showing cells · wires · depth, with a
green tick when it succeeded and a red error icon (plus the first error message)
when it did not. Expanding a module breaks it down by cell type, sorted by
count descending — this is the quickest way to see what a design actually
mapped to.
Two inline icons appear on a module row when the corresponding artefact exists: open its synthesized Verilog, or open its schematic diagram.
After Place & Route, three extra sections are appended:
| Section | Contents |
|---|---|
| Timing | Max frequency, pre-route estimate (when it differs), critical-path delay, setup and hold slack, and whether constraints were MET or MISSED |
| Utilization | LUTs, registers, BRAM, DSP, IO — each as used / total with a percentage |
| Critical Paths | Each path as from → to, expandable into its individual steps |
Rows appear only when nextpnr actually reported that metric, so the exact set
varies by family and design. Utilization categories with a total of zero are
omitted rather than shown as 0 / 0.
Critical Paths is capped at the five worst paths to keep the tree usable on large designs with many cross-domain paths. When you need the complete list, read
report.jsonin the run’s04-nextpnr/directory.
Those three sections are cleared whenever you re-run Elaborate or Synthesize. That is deliberate: neither command produces place-and-route numbers, so keeping them would leave stale Fmax and utilization figures on screen next to fresh synthesis results.
History
Every run is written to its own timestamped directory under .clash/, so
nothing is overwritten. This view reads them back from disk — including runs
from previous sessions.
The tree is three levels deep:
Example.Project.topEntity ← function, with a run count
└─ 2026-07-28_14-31-05 ← run id (its timestamp)
├─ top_entity ← module, with cells/wires/depth
└─ accum
A run row summarises itself: the command that produced it, the target (when not
generic), cell count, and Fmax where applicable, with a tick or error icon
from the recorded outcome. Hovering shows the full timestamp, target, cells, and
Fmax. Runs whose run.json is missing or unreadable still appear, marked
no metadata.
Clicking a run loads it back into Results, so you can inspect an old run exactly as if it had just finished — the Results header labels which run is being shown.
Inline icons differ by row type:
- On the History header: clear the whole history — every design’s runs.
- On a design row: delete that design’s history, all of its runs at once.
- On a run row: delete the run, which removes its output directory from disk.
- On a module row: open that module’s Verilog, or its diagram — each icon appears only when the artefact exists on disk.
Every deletion asks first, saying what will be removed, and takes the files with
it — these are directories under .clash/, not entries in a list. The generated
cabal project in .clash/synth-project/ is never touched; it is not run output.
A module row’s Verilog is the file Clash generated for that component. Clash writes one directory per component under
02-verilog/, so a component that produced no Verilog of its own simply has no icon.
Use Refresh in the title bar after changing .clash/ outside the editor
(Clash: Refresh Run History in the palette refreshes only this section). The
synth-project/ directory is skipped when enumerating
functions, since it holds the generated cabal project rather than run output.
See Output Directory Structure for the on-disk layout these entries map to.
Commands
The main commands are available from the VS Code command palette
(Ctrl+Shift+P). Commands that act on a specific tree item are reachable only
from the Clash Synthesis sidebar — its view title bar and right-click
menus — and are hidden from the palette.
Main flow
| Command | Description |
|---|---|
| Clash: Detect Functions | Scan the current file for functions, show which are synthesisable |
| Clash: Generate Verilog | Generate a wrapper module and compile to Verilog with Clash |
| Clash: Elaborate | Clash → Yosys elaboration only (no tech mapping), one diagram per module |
| Clash: Synthesize | Full Clash → Yosys pipeline without place & route. Optional out-of-context mode for per-module diagrams |
| Clash: Place & Route | Full pipeline: Clash → Yosys → nextpnr |
Toolchain
| Command | Description |
|---|---|
| Clash: Check Toolchain | Probe every external tool (cabal, Yosys, nextpnr) and report what is reachable |
| Clash: Install Toolchain | Pick which EDA tools the extension should download and manage itself |
| Clash: Install the Haskell Extension (for HLS) | Reveal the Haskell extension in the Marketplace view. Function detection needs HLS, so this is also the action offered on the Functions row when the extension is missing |
Sidebar
| Command | Where | Description |
|---|---|---|
| Clash: Refresh | Sidebar title bar | Re-read the active file’s functions and the runs on disk |
| Clash: Refresh Haskell Functions | Palette | Re-scan the active file for functions only |
| Clash: Go To Function | Click a Functions item | Jump to the function’s definition |
| Clash: Open Settings | Gear icon in the title bar | Open the settings panel (synthesis target and scripts) |
| Clash: Open Synthesized Verilog | Results item, inline icon | Open the Yosys-synthesized Verilog |
| Clash: View Module Diagram | Results item, inline icon | Open a module’s schematic SVG |
| Clash: View Component Diagram | Click a sub-component row under a module | Open that component’s own schematic (rendered on first open) |
| Clash: Refresh Run History | Palette | Re-read past runs from .clash/ only |
| Clash: Show Run in Synthesis Results | Click a History item | Load a past run into the Results section |
| Clash: Open Verilog (History) | History item, inline icon | Open a past run’s Verilog |
| Clash: View Diagram (History) | History item, inline icon | Open a past run’s diagram |
| Clash: Delete Run | History run row, inline icon | Delete that run’s output directory |
| Clash: Delete Design History | History design row, inline icon | Delete every run recorded for that design |
| Clash: Clear Run History | Trash icon on the History header | Delete the run history of every design |
The four main-flow commands (Generate Verilog, Elaborate, Synthesize, Place & Route) also appear as icons in the sidebar title bar, so you can drive the whole flow from the sidebar without the palette.
Detect Functions
Scans the current Haskell file (or all open Haskell documents) using HLS document symbols and hover types. Shows a picker listing each function with its type signature, marking monomorphic functions with ✓ and polymorphic ones with ✗.
If you select a monomorphic function, you’re offered the option to synthesize it immediately.
Generate Verilog
An interactive command that:
- Detects functions in the current file
- Shows a picker with only synthesizable (monomorphic) functions
- Generates a Clash wrapper module
- Compiles to Verilog with Clash
- Optionally runs Yosys synthesis
Synthesize
Runs the full Clash compilation and Yosys synthesis pipeline without place & route. This is useful when you want to inspect synthesis results and circuit diagrams without targeting a specific FPGA.
Respects the outOfContext setting:
- disabled (default) — the whole design is synthesized as a single netlist
- enabled — each component is synthesized standalone (out of context), producing its own
.il(RTLIL),.json(netlist), and.svg(diagram) plus per-component stats. This path runs a fixed generic script with no technology mapping, so the target does not apply, a component’s figures include its descendants, and nothing is optimized against its parent — see Configuration
Elaboration (Clash: Elaborate) always runs per-module regardless of this setting — its goal is to give a faithful per-component view of what Clash produced.
Place & Route
The full FPGA implementation pipeline. After detecting and selecting a function:
- Generates wrapper module
- Compiles to Verilog with Clash
- Synthesizes with Yosys for the configured target
- Reads the top entity’s target clock frequency from the Clash manifest
- Runs the target’s
nextpnr-*binary with the selected device and package
The FPGA family comes from the synthesisTarget setting, not from a prompt.
Place & route is available for ecp5, ice40, and gowin; with any other
target (generic, xilinx, quicklogic, sf2) the command reports that P&R
is unavailable and stops, because no nextpnr binary is wired up for it.
Once the family is known you’re prompted for a device, and then for a package if that device offers a choice. The device list is family-specific — see Nextpnr Integration for the full set.
Code Actions
The extension registers a code action provider for Haskell files. When your cursor is on a monomorphic function definition, pressing Ctrl+. (or clicking the lightbulb) offers:
- Clash: Elaborate ‘funcName’ — Clash compilation + Yosys elaboration, one diagram per module
- Clash: Synthesize ‘funcName’ — runs Clash compilation + Yosys synthesis (no PnR)
- Clash: Place & Route ‘funcName’ — full Clash + Yosys + nextpnr pipeline
Code actions skip the function detection and picker dialogs — the function under the cursor is used directly. This provides a fast workflow for synthesizing specific functions without navigating the command palette.
How It Works
The provider calls FunctionDetector.getFunctionAtPosition() — a targeted single-symbol lookup rather than a full document scan, so it is cheap enough to run on every cursor move. Only monomorphic functions produce code actions; polymorphic functions and non-function positions are silently ignored.
Because the lookup is asynchronous, the provider also honours the cancellation token: if the cursor moves again before the lookup resolves, the superseded request returns no actions rather than building them from a stale position.
Configuration
All settings live under clash-toolkit in VS Code settings, grouped into
Synthesis, Place & Route, Build, Toolchain and Yosys Scripts.
| Setting | Default | Description |
|---|---|---|
synthesisTarget | generic | Target FPGA family for Yosys synthesis. One of generic, ice40, ecp5, xilinx, gowin, quicklogic, sf2. Also selects the nextpnr binary for Place & Route |
outOfContext | false | Out-of-context synthesis: when enabled, each component in a multi-component design is synthesized standalone with its own diagram + utilization stats |
pnrWriteRoutedSvg | true | Write a routed-layout SVG alongside the nextpnr output, showing where the design landed on the fabric |
cabalJobs | auto | Packages cabal may build at once (--jobs). auto is one job per core; a number caps it; 1 builds sequentially |
ghcJobs | (unset) | Modules GHC may compile in parallel within a package (--ghc-options=-jN). Leave blank to keep GHC single-threaded — the only job setting that is not auto by default, see Build parallelism |
yosysJobs | auto | Components Yosys may synthesize at once on the per-component path. auto is one process per core less one, capped at 8 |
nextpnrThreads | auto | Threads nextpnr may use (--threads). auto is one per core less one, capped at 4 |
toolCommands | {} | Per-tool command overrides, keyed by tool name (cabal, yosys, nextpnr-*) |
elaborationScript | (built-in) | Custom Yosys script for the elaboration stage |
outOfContextScript | (built-in) | Custom Yosys script run once per component while outOfContext is on |
synthesisScript.<target> | (built-in) | Custom Yosys script per target — one setting for each of the seven targets above |
The P&R target frequency is not a setting. It is the period of the top entity’s clock domain, read from the Clash manifest on every run — see Timing Analysis.
Tool Commands
The extension finds its tools on PATH, and offers to download and manage them
(Clash: Install Toolchain) when they aren’t there. toolCommands is the
escape hatch for what neither route reaches — a binary somewhere unusual, or a
wrapper that has to run in front of it:
"clash-toolkit.toolCommands": {
"yosys": "/opt/oss-cad-suite/bin/yosys",
"cabal": "nix run nixpkgs#cabal-install --",
"nextpnr-ecp5": "wsl nextpnr-ecp5"
}
Keys are tool names: cabal, yosys, nextpnr-ecp5, nextpnr-ice40,
nextpnr-himbaechel. Values are split on spaces, so anything after the first
token becomes leading arguments; quote a path that contains spaces. An entry you
don’t set means “run the tool by name”, which is what leaves detection and the
managed download in charge.
The same command is used for the pre-flight toolchain probe and for the actual run, so “the check passes but synthesis spawns something else” cannot happen.
yosysCommandwas the single-tool ancestor of this setting. It is deprecated but still honoured; move it totoolCommandsunder the keyyosys.
Custom Yosys Scripts
Every synthesis target ships a built-in Yosys script, and each can be overridden:
elaborationScript for the elaboration stage, outOfContextScript for the
per-component path, and synthesisScript.generic, synthesisScript.ice40,
synthesisScript.ecp5, synthesisScript.xilinx, synthesisScript.gowin,
synthesisScript.quicklogic, and synthesisScript.sf2 for whole-design
synthesis. An empty string means “use the built-in default”, so clearing a
setting reverts it.
Scripts are expanded with these placeholders before Yosys runs:
| Placeholder | Expands to |
|---|---|
{files} | The Verilog files to read |
{topModule} | The top module name |
{outputDir} | The stage’s output directory |
{outputBaseName} | Base name for generated output files |
{libFiles} | (out-of-context only) the sub-components to read as black boxes |
{keepBlackBoxes} | (out-of-context only) keeps those instances through optimization |
The easiest way to edit these is Clash: Open Settings (the gear icon in the sidebar), which shows the active script and an inline diff against the default so you can see exactly what you changed.
Which script the panel is editing
outOfContextScript and synthesisScript.<target> are separate scripts, not
variants of one. An out-of-context run stubs its sub-components and issues no
synth_* command, so the target’s script has nothing to say about it — and
editing one does not affect the other.
The settings panel follows the Out-of-context checkbox: tick it and the
editor, the modified badge and the diff all switch over to outOfContextScript;
untick it and they switch back to the selected target’s script. What the editor
shows is always the script the next run will execute.
Where scripts still do not apply. Elaboration of a design with more than one component builds its own fixed per-component script, so a custom
elaborationScripthas no effect there. See Elaboration below.
Use abc9, not abc
A custom script that maps logic should reach for abc9 (or the -abc9
option of a synth_* command). The legacy abc flow can hang: Yosys drives abc
as a co-process over a pipe, and when abc finishes it prints its interactive
prompt and waits for a command that never arrives, while Yosys waits for more
output. Nothing breaks visibly — the run simply stops making progress until it
fails with Yosys timed out after 600s.
Whether it happens is a matter of timing, so a script can work on a quiet
machine and hang on a busy one. The abc9 flow hands abc a script file instead,
so abc exits on its own and cannot deadlock. The built-in scripts all use it;
the xilinx and sf2 defaults were changed to it in 0.5.2 for this reason.
Out-of-Context Synthesis
Disabled (default)
The whole design is synthesized as a single netlist with target-specific commands (e.g. synth_ecp5). Produces one JSON netlist and one synthesized Verilog file. This matches what nextpnr consumes for place-and-route.
Enabled
Each component in the dependency graph is synthesized independently, producing:
- An
.il(RTLIL) file per component - A
.jsonnetlist per component - An
.svgcircuit diagram per component - Per-component statistics (cell count, wire count, logic depth)
Useful for inspecting and comparing each component’s synthesis result on its own. The Place & Route command always uses the whole-design path regardless of this setting; nextpnr needs a merged netlist.
What actually runs. Each component is synthesized out of context with the
outOfContextScript template — proc, opt -purge, memory -nomap, opt,
with no flatten and no technology mapping. Its sub-components are read
with read_verilog -lib, which keeps their port interfaces and discards their
bodies, so they become opaque black boxes. No synth_* command runs, so:
- The target’s script does not apply here. The cells counted are generic
Yosys cells (
$add,$dffe,$mem_v2, …), not the target’sLUT4/TRELLIS_FF/ block RAMs. On the test design, whole-designecp5synthesis reports 173 cells (CCU2C,LUT4,TRELLIS_FF) while the same design’s components report generic cells. The script this path does use isoutOfContextScript, which is editable — see Which script the panel is editing. - A component’s figures cover its own logic. Each sub-component counts as one opaque cell rather than being expanded, so the numbers describe that component and nothing below it.
- Nothing is optimized against the parent. A component never sees the design above it, so constants the parent would feed in aren’t propagated and logic the parent leaves unused isn’t pruned. This is where most of the gap against a whole-design run comes from, and it can be large: on a small two-instance test design, blocking constant propagation across one boundary took the cell count from 469 to 818.
Use the numbers to compare components with each other, not to predict whole-design utilization — for that, run with this setting off.
Two details of the default script are load-bearing:
- No technology mapping. A full
synthper component hangs indefinitely on components containing large block RAMs, becausememory_mapplusabccannot finish on the resulting flip-flop array. Keeping memories as$memcells avoids that. {keepBlackBoxes}. Yosys deletes a black-box instance whose outputs happen to be unused. Drop this line from a custom script and such a component disappears from the diagram and the cell counts without any warning.
Components run in parallel. A component’s run needs its dependencies’
Verilog, never their results, so there is no ordering constraint between
them and they are all dispatched concurrently — yosysJobs at a time. This
applies to per-component elaboration too.
The extension says so where those numbers appear: out-of-context rows in
Results and History are tagged out of context with the caveat in their
tooltip, the Results section header names the mode, and the output channel
repeats it at the start of the run.
Hierarchy is preserved in the view. Although each component is synthesized
standalone, the results are still presented as the design’s hierarchy — the top component at the root, the components it
instantiates nested beneath it — so the view reads the same whether or not this
setting is on. The graph comes from the Clash manifest and is recorded in
per-module/hierarchy.json, which is also what lets History rebuild the same
nesting for a past run.
Elaboration
The Clash: Elaborate command always runs per-component — its purpose is to
expose what Clash produced before technology mapping, so each component’s
hierarchy is preserved and rendered with sub-component instances shown as boxes.
The outOfContext setting does not affect elaboration.
Elaboration reads its dependencies in full rather than as black boxes: its
netlist has to carry the real sub-module definitions so the diagram can be
drilled into. Each component is then run through proc and opt_clean only —
no flatten — so its diagram covers that component alone with sub-components
shown as instances.
elaborationScript applies to the whole-design path — a single-component design.
For a design with more than one component, the per-component script above is used
instead and a custom elaborationScript has no effect.
Clash Invocation
The extension invokes Clash via: cabal run --jobs=$ncpus clash-synth:clash --
This runs the clash executable from the synthesis cabal project at .clash/synth-project/, which depends on your package through cabal. This ensures all transitive dependencies are resolved correctly.
The synth project is created and updated automatically — you don’t need to manage it.
Build parallelism
The first run of a project has to build its dependency tree, and cabal builds
one package at a time unless told otherwise. cabalJobs is what tells it
otherwise — it defaults to auto, which cabal spells $ncpus: as many
packages at once as you have cores. Set a number to leave headroom for the rest
of the machine, or 1 to go back to a sequential build. Changing it never
invalidates anything cabal has already built.
cabalJobs only parallelises across packages, so it cannot speed up the build
of a single large package — your own, typically. ghcJobs does that, by passing
-jN to GHC so it compiles that many modules at once. It is off by default for
two reasons: GHC options are part of cabal’s build plan, so turning it on (or off
again) changes every package’s identity and rebuilds the whole plan once; and a
Clash design’s cost tends to sit in the one module holding topEntity, which
parallelising across modules cannot split.
Tool parallelism
Every tool that can be parallelised takes the same shape of setting. auto — the
default everywhere except ghcJobs — derives a count from the machine: one job
per core, less one for the extension host and the editor, then capped. A positive
integer overrides it and is not capped, since someone who names a number has
said something about their machine that the cap has no business overruling.
| Setting | Unit of work | auto cap | Why the cap |
|---|---|---|---|
cabalJobs | packages | (cabal decides) | passed through as cabal’s own $ncpus |
ghcJobs | modules in a package | (off by default) | see above |
yosysJobs | whole components | 8 | past that the runs contend for memory, not CPU — each holds a whole design |
nextpnrThreads | nextpnr’s threaded passes | 4 | only some passes thread, and routing stops gaining well before the core count |
An invalid value (blank, zero, a fraction, a word) falls back to auto rather
than failing the run.
nextpnrThreadsinteracts with reproducibility: the passes nextpnr threads are the ones whose result can depend on scheduling, so a fixed--seedonly pins a run’s outcome at a fixed thread count. SetnextpnrThreadsto1if you need bit-identical results across machines.
The Settings Panel
Clash: Open Settings — the gear icon in any of the three sidebar view title
bars — opens a dedicated panel titled Clash Synthesis Settings. It is a
friendlier front end for the settings described in
Configuration, and the only practical way to edit the Yosys
scripts, since they are multi-line strings that are awkward to write in
settings.json.
The panel and VS Code settings stay in sync: edits made in settings.json are
reflected here as soon as the configuration changes.
Tools
A live view of the toolchain. Three tools are probed — cabal, Yosys, and
nextpnr-ecp5 — each listed with:
- whether it was found,
- its version string,
- the path it resolved to,
- or the error if the probe failed,
alongside the explanation of what that tool is needed for. Refresh re-probes
everything from scratch rather than reusing the cached result, which is what you
want after installing a tool or changing toolCommands.
The other place-and-route binaries (
nextpnr-ice40,nextpnr-himbaechel) are not in this list. They are checked on demand when Place & Route runs for their target, so this panel showing onlynextpnr-ecp5does not mean the others are missing.
This is the same information Clash: Check Toolchain reports, in a form you can leave open while fixing your environment. See Managed Toolchain for what to do about anything missing.
Elaboration
The Yosys script used by Clash: Elaborate — hierarchy and proc only, no
technology mapping, producing a word-level netlist of generic cells.
The script is editable in place. A modified badge appears when it differs from the built-in default, and Reset to Default clears the override.
Synthesis
Three controls:
- Target — the FPGA family, matching
synthesisTarget. Changing it switches which script the editor below is showing. - Out-of-context — the
outOfContexttoggle (see Configuration). This also switches the editor, see below. - Synthesis script — the Yosys script the next run will execute, with the same modified badge and Reset to Default button as the elaboration script.
Because each target has its own script setting, switching the dropdown and editing affects only that target. Overrides for the others are left untouched.
The editor follows the checkbox
Out-of-context synthesis runs a different script, not a variant of the
target’s: it stubs the component’s sub-components as black boxes and issues no
synth_* command. It is stored separately, as outOfContextScript.
So the editor is bound to whichever of the two the current settings would
actually run. Tick Out-of-context and the title changes to Out-of-Context
Synthesis Script, and the contents, the modified badge and the diff all
switch to outOfContextScript — along with two extra placeholders, {libFiles}
and {keepBlackBoxes}, that only mean anything on that path. Untick it and
everything switches back to the selected target’s script. Save and Reset
to Default always act on whichever is on screen.
Unsaved edits survive an unrelated settings change, but switching the target or the checkbox reloads the editor — it is bound to a different script at that point, so there is nothing for the old text to be saved to.
The Inline Diff
When a script differs from its default, the panel shows a line-by-line diff underneath the editor — added lines, removed lines, and unchanged context.
This matters more than it might sound. The default scripts do real work beyond
calling synth_*: they assert the design has no unconnected or multiply-driven
wires, emit the stats.json the extension parses for the sidebar, write the
logic_depth.txt report, and render the diagram via show. It is easy to break
one of those by accident while customising, and the diff makes it obvious what
you have changed.
If a custom script drops the
tee -q -o "{outputDir}/stats.json" stat -jsonline, synthesis fails loudly rather than silently reporting no statistics — the extension treats a missingstats.jsonas an error.
Remember that scripts are templates: {files}, {topModule}, {outputDir},
and {outputBaseName} are substituted before Yosys runs. {files} becomes one
quoted read_verilog line per input file, so paths containing spaces are safe.
Circuit Diagrams
The extension renders circuit diagrams as SVGs with netlistsvg, then opens them with VS Code’s built-in image preview editor.
Every Yosys script writes a JSON netlist (write_json); netlistsvg reads that
netlist, lays it out with ELK, and draws cells
from an SVG “skin” — so schematics look like schematics rather than a graph of
labelled boxes. netlistsvg ships inside the extension, so there is no diagram
tool to install: if synthesis produced a netlist, you get a diagram.
Rendering runs in a background process, so laying out a large design never freezes VS Code. Opening a diagram waits for its render to finish if one is still in flight.
Viewing diagrams
Diagrams open as preview tabs, so walking a hierarchy replaces the diagram on screen instead of leaving a tab behind for every component you looked at. To keep one while you open the next, pin its tab (or double-click it) — the next diagram then opens alongside rather than over it.
- After Clash: Elaborate, the diagram opens automatically and one diagram is produced per module.
- After Clash: Synthesize, the diagram opens automatically. With
outOfContextenabled, each module gets its own diagram; otherwise a single whole-design diagram is rendered. - Click the diagram icon next to any module in the sidebar’s Results section (or under a run in History) to (re-)open that module’s diagram.
Drilling into sub-components
A diagram draws one module. The components it instantiates appear as boxes with their ports — not expanded into gates — which keeps the diagram readable but means the box alone tells you nothing about what is inside.
To go inside one, expand the module’s row in Results (or under a run in History): every component it instantiates is listed beneath it, and those rows expand in turn, so a deep hierarchy is walkable level by level. Clicking a component opens its own diagram.
top_entity 173 cells · 412 wires
├─ accum component ← click to open accum's diagram
├─ pipelined_sum component
│ └─ mult_unsigned component ← expands as far as the hierarchy goes
└─ TRELLIS_FF 32 ← cell-type breakdown, as before
Those diagrams are rendered the first time you open one — laying out every
module of a design up front would cost far more than it’s worth — and cached in
a diagrams/ directory beside the netlist. Primitives ($add, $dff, …) and
black-box library cells (LUT4, TRELLIS_FF, DP16KD, …) are not listed:
they have no internals to draw.
Which components are listed depends on what survived synthesis. Elaborate
keeps the whole hierarchy. Synthesize keeps it for the generic target, but
the vendor targets (synth_ecp5, synth_ice40, …) flatten the design, so there
is nothing left to drill into — use Elaborate to see the structure.
outOfContext also gives you a row per component, but by a different route:
each component was synthesized as a run of its own, so its row carries its own
statistics and its own pre-rendered diagram instead of being drawn on demand from
the parent’s netlist. Those components are flattened individually, so a
component’s diagram shows its whole subtree inlined rather than sub-component
boxes — the hierarchy lives in the tree, not in the picture. Such rows are tagged
out of context; see
Configuration for what that means
for the numbers.
Per-module diagrams
The Elaborate command always produces one diagram per component. The top component’s diagram preserves the hierarchy: sub-component instances are rendered as boxes rather than expanded into gates. Each sub-component has its own diagram showing its own internals.
For Synthesize, set clash-toolkit.outOfContext to true to get the same per-component breakdown, with each component synthesized standalone (so you also see per-component statistics — which count generic cells, since that path does no technology mapping; see Configuration).
Elaborate for readable schematics
Elaborated netlists still carry word-level cells ($add, $mux, $dff, …),
which netlistsvg draws as recognisable symbols. A technology-mapped netlist has
been shredded into hundreds of LUTs and flip-flops, so its diagram is faithful
but far harder to read. Use Elaborate to understand a design’s structure and
Synthesize to see what actually got mapped.
Troubleshooting
“Diagram not available — rendering it failed”
The netlist was there but netlistsvg could not draw it; the output channel has
the error. Very large whole-design netlists are the usual cause — either enable
outOfContext to render sub-modules individually, or use Elaborate for a
higher-level diagram.
“No diagram for this module — the run produced no JSON netlist to render”
There was no netlist to render from. If you edited the synthesis script (see
Configuration), check that it still has its
write_json "{outputDir}/{outputBaseName}.json" line — that file feeds both the
diagram and place-and-route.
Design was optimized away
If Yosys’s optimization passes removed everything (e.g. constant outputs), the diagram will be empty. Check .clash/<module>/03-yosys/yosys.log.
Timing Analysis
After place & route with nextpnr, the extension reports timing information.
Metrics
| Metric | Meaning |
|---|---|
| Pre-Routing Frequency | Estimated FMax before routing — optimistic upper bound |
| Max Frequency | Actual FMax after routing — the real achievable clock speed |
| Critical Path Delay | The longest combinational path in nanoseconds |
| Constraints Met | Whether the design meets the target frequency |
The routing overhead (difference between pre-routing and post-routing frequency) is typically 15–30% and is normal.
Target Frequency
The frequency nextpnr is constrained against comes from the Clash manifest,
which states everything needed: which top-entity ports are clocks, which domain
each one is in, and every domain’s period. A top entity whose clock port is in
Dom50 (period: 20000 ps) is placed and routed against 50 MHz, passed as
--freq.
This is deliberately not a setting. The target is a property of the entity being synthesized — two top entities in one workspace can run at different frequencies, and the manifest already states both.
Nothing is guessed around, because the number becomes a verdict: a frequency belonging to some other part of the design would have place & route report constraints met about a constraint the design never had. So:
| The manifest says | What happens |
|---|---|
| One clock domain across the top entity’s clock ports | That domain’s period is the target |
| No clock ports at all | No --freq — nextpnr reports an unconstrained Fmax, which is what a combinational design has |
| Clock ports in two or more domains | Place & route stops, naming each clock and its frequency. One --freq covers the whole design, so no single number can be met by both |
| A clock port with no domain, a domain the manifest never defines, or a domain without a usable period | The manifest is rejected when parsed — it disagrees with itself |
For a multi-clock design, synthesis and elaboration still work normally; it is only place & route that has nothing to constrain against.
Resource Utilization
The extension also reports resource utilization after place & route:
- LUTs — Look-up tables used vs. total available
- Registers — Flip-flops used vs. total
- BRAM — Block RAM tiles used vs. total
- IO — IO pins used vs. total
All values include usage percentages.
Architecture Overview
Source Layout
src/
extension.ts Activation, command registration, orchestration
── Front end (finding what to synthesize) ──
clash-code-actions.ts Code action provider (Ctrl+. on functions)
hls-client.ts HLS integration (document symbols, hover types)
function-detector.ts Function scanning and classification UI
type-analyzer.ts Monomorphism analysis
── Pipeline ──
code-generator.ts Wrapper generation, synth project, run directories
clash-compiler.ts Clash invocation and output parsing
clash-manifest-parser.ts clash-manifest.json parsing, clock-domain analysis
clash-manifest-types.ts Types for manifest data structures
yosys-runner.ts Yosys script generation and execution
yosys-types.ts Types for Yosys synthesis results
synthesis-targets.ts Target registry, default scripts, placeholder expansion
nextpnr-runner.ts nextpnr invocation, timing/utilisation parsing
nextpnr-types.ts Families, device tables, options and results
── Tooling ──
toolchain.ts External tool availability checking
tool-provider.ts Managed OSS CAD Suite download and path resolution
── Diagrams ──
netlist-renderer.ts netlistsvg rendering; also the child-process entry point
netlist-diagram.ts Render orchestration, component hierarchy queries
── UI ──
clash-tree.ts The sidebar view; routes each section to its provider
haskell-functions-tree.ts Functions section
synthesis-results-tree.ts Results section
run-history-tree.ts History section
run-loader.ts Reads a past run back off disk
synthesis-settings-panel.ts Settings webview (tools, scripts, inline diff)
── Support ──
file-logger.ts Debug file logging (.clash/debug.log)
types.ts Shared FunctionInfo interface
Key Types
interface FunctionInfo {
name: string;
range: Range;
typeSignature: string | null;
isMonomorphic: boolean;
filePath: string;
moduleName: string | null;
}
interface ComponentInfo {
name: string;
verilogFiles: string[];
dependencies: string[]; // direct only, not transitive
directory: string;
}
type PortAnnotation =
| { kind: 'name'; name: string }
| { kind: 'product'; name: string; subPorts: string[] };
Data Flow
User Code (.hs)
│
▼
HLS (symbols + hover)
│
▼
FunctionDetector → TypeAnalyzer
│
▼
CodeGenerator (wrapper .hs + synth project)
│
▼
ClashCompiler (cabal run clash → Verilog)
│
▼
ClashManifestParser (manifest + target frequency)
│
▼
YosysRunner (synthesis script → netlist JSON)
│
├──────────────▶ netlist-diagram → netlistsvg (netlist JSON → SVG)
▼
NextpnrRunner (PnR → timing)
Extension Activation
On activation (onLanguage:haskell):
- Create the “Clash Synthesis” output channel
- Initialize the file logger at
.clash/debug.log - Instantiate the pipeline components (HLSClient, FunctionDetector,
CodeGenerator, ClashCompiler, YosysRunner, NextpnrRunner) and the
clashdiagnostic collection - Initialize the managed tool provider, then the ToolchainChecker
- Create the sidebar view. The three tree providers are instantiated as before
and handed to
ClashTreeProvider, which contributes the Functions / Results / History section headers and routes every call back to whichever provider produced the row (each returned node is stamped with its section, sinceSubComponentItemrows can come from two of them). It is registered withcreateTreeViewrather thanregisterTreeDataProviderfor its.selectionproperty, which title-bar buttons read to find the selected function - Subscribe to active-editor changes so the functions view follows the current Haskell file
- Register commands and the code action provider for Haskell files
- Run toolchain validation after a 2-second delay (to allow direnv)
Synthesis Pipeline
Wrapper Generation
The CodeGenerator creates a Clash wrapper module that re-exports the user’s function as topEntity with a Synthesize annotation. The wrapper is written to .clash/synth-project/src/.
Port names are derived heuristically from the type signature:
| Type pattern | Port annotation |
|---|---|
Clock … | PortName "CLK" |
DiffClock … | PortProduct "CLK" [PortName "p", PortName "n"] |
Reset … | PortName "RST" |
Enable … | PortName "EN" |
| Anything else | PortName "INA", PortName "INB", … |
| Output | PortName "OUT" |
The synthesis cabal project (ensureSynthProject) maintains cabal.project, clash-synth.cabal, and bin/Clash.hs. It discovers the user’s cabal project via findCabalProject and adds it as a dependency.
Clash Compilation
ClashCompiler.compileToVerilog() runs:
cabal run clash-synth:clash -- <ModuleName> --verilog
with --project-dir and --project-file flags when a user cabal project is detected. The compiler parses stdout/stderr for errors and warnings, and locates the generated Verilog and clash-manifest.json.
Yosys Synthesis
The runner exposes three flows, all sharing the same Yosys child-process plumbing:
Whole-design (synthesize)
Default for Synthesize and always used for Place & Route. Generates a single Yosys script that reads every Verilog file, elaborates the hierarchy, runs target-specific synthesis (synth_ecp5, synth_ice40, etc.), and writes outputs (synthesized Verilog, netlist JSON, statistics, diagram).
Per-module synthesis (synthesizePerModule)
Used by Synthesize when outOfContext is enabled. Each component in the dependency graph is synthesized independently with its own directory under per-module/<name>/, from the outOfContextScript template — its own setting, separate from synthesisScript.<target>, since this path issues no synth_* command at all:
- The component’s direct dependencies are read with
read_verilog -lib, which keeps their port interfaces and discards their bodies. They become black boxes, sohierarchy -checkpasses without their contents being elaborated. One level of stubs is enough however deep the design goes: a black box has no body, so the components it instantiates are never referenced - The component is optimized standalone —
proc,opt -purge,memory -nomap,opt, with noflatten. There is also no technology mapping: a fullsynthper component hangs on components with large block RAMs (memory_map+abcon the resulting flip-flop array), so the target’ssynth_*command and any custom script are not used on this path - Black-box instances get
setattr -set keep 1 t:<dep>right afterproc. Without it,opt/opt_clean/cleandelete any instance whose outputs happen to be unused, and the component would silently vanish from the diagram and the cell counts - Each component produces
.il(RTLIL),.json(netlist),.svg(diagram), and per-component statistics whose cells are generic, cover only that component’s own logic (one opaque cell per sub-component), and are not comparable with a whole-design run - The component graph is written to
per-module/hierarchy.jsonso both sidebar views can present the results as the design hierarchy, without either view having to read it back out of a netlist
Because a component’s run needs its dependencies’ Verilog and never their results, the components have no ordering constraint between them and are all dispatched concurrently — see Concurrency below.
Per-module elaboration (elaboratePerModule)
Always used by Elaborate. Same per-module driver as synthesizePerModule, but dependencies are read in full (not -lib) and the script body is proc + opt_clean (no flatten, no tech mapping). The netlist therefore carries the real sub-module definitions, so its diagram can be drilled into; instances still appear as boxes rather than being expanded.
Concurrency
Both per-module flows run through mapPool, which keeps up to
perModuleConcurrency() Yosys processes in flight. That resolves the
clash-toolkit.yosysJobs setting through the shared resolveJobCount in
parallelism.ts:
auto means one per core minus one for the editor, capped at 8 (past that the
runs contend for memory rather than CPU, and a design with a large block RAM can
hold a lot of it per process); an explicit number is honoured uncapped; and the
result never exceeds the component count. The same helper backs cabalJobs,
ghcJobs and nextpnrThreads.
Results are returned in input order regardless of completion order, so
moduleResults stays in the manifest parser’s dependency order (leaves first,
top last) and the “combined” result at the end of the array is still the top
component. Cancelling stops further components being scheduled; runs already in
flight are killed through the abort signal inside runYosysScript.
Diagram Rendering
Yosys writes no diagram of its own; netlist-renderer.ts renders one from the
JSON netlist the script already emits, using the bundled
netlistsvg library (ELK for layout, an
SVG skin for the cell symbols). The netlist’s top attribute decides which
module is drawn, and the per-module flows override it with the component name so
the netlist’s dependency modules don’t win.
netlist-diagram.ts orchestrates that:
- renders in a forked child process (
out/netlist-renderer.jsrun as a script), because ELK layout is CPU-bound JavaScript that would otherwise block the extension host for seconds on a large design; - fire-and-forget — synthesis resolves as soon as Yosys exits, and each render
is registered by its target path so
waitForSvg()can join it at the point of use (opening a diagram) instead of at the point of creation; - degrades to a warning in the output channel, never a failed synthesis; if the child cannot be spawned at all it falls back to rendering in-process.
nextpnr Place & Route
NextpnrRunner.placeAndRoute() builds command-line arguments for the selected FPGA family and device, runs nextpnr, and parses timing and utilization from stdout.
The manifest-derived target frequency is passed via --freq when the top entity has a clock.
Output Directory Structure
All generated files live under .clash/ in the workspace root.
Because that lands inside your repository, the extension offers once per
workspace to add .clash/ to an existing .gitignore — Yes writes the entry
with a comment saying what it is, No is remembered and never asked again, and
Not right now leaves the question open for the next session. The offer is
skipped entirely when the workspace has no .gitignore (the extension does not
create one) or when the file already mentions .clash. To change a No later,
add the entry yourself; the extension will then see it and stay quiet.
Each invocation writes into its own timestamped run directory, so previous results are never overwritten. This is what the History section of the sidebar reads.
.clash/
debug.log Debug log for all tool invocations
debug.log.old Previous session's log, rotated on activation
synth-project/ Cabal project that depends on your package
cabal.project
clash-synth.cabal
bin/Clash.hs
src/ Generated wrapper modules
{Module}.{Function}/ One directory per synthesised function
runs/
{YYYY-MM-DD_HH-MM-SS}/ One directory per run, id is its timestamp
run.json Run summary — command, target, stats, Fmax
02-verilog/ Clash Verilog output
{Module}.topEntity/
function_name.v Main Verilog
clash-manifest.json Clash metadata
*.sdc Timing constraints
…
03-yosys/ Yosys synthesis results
function_name_synth.v Synthesized Verilog
function_name.json JSON netlist (for nextpnr and diagrams)
function_name.svg Schematic diagram (netlistsvg)
diagrams/ Sub-component diagrams, rendered on demand
{Module}.svg
stats.json Machine-readable statistics (`stat -json`)
statistics.txt Human-readable statistics report
logic_depth.txt Longest topological path (`ltp`)
synth.ys Yosys script
yosys.log Complete Yosys output
per-module/ Per-module synthesis outputs
hierarchy.json Component graph + out-of-context flag
{Module}/
{Module}.il RTLIL
{Module}.json JSON netlist
{Module}.svg Schematic diagram (netlistsvg)
synth.ys
yosys.log
04-nextpnr/ Place & route output
function_name.config Textual FPGA configuration
function_name.routed.svg Routed-layout SVG (when pnrWriteRoutedSvg)
report.json Machine-readable timing/utilisation report
nextpnr.log
The run id is a local-time timestamp (formatRunId), so runs sort
chronologically by name. synth-project/ is deliberately excluded when the Run
History view enumerates function directories.
Clash Manifests
Clash generates a clash-manifest.json in each HDL output directory. The extension parses this to:
- Determine the top component name and ports
- Discover dependencies between components
- Extract clock domain information
- Collect all Verilog files (including sub-modules)
Manifest Structure
{
"components": ["top_entity"],
"dependencies": { "transitive": ["Example.Project.accum"] },
"domains": {
"Dom50": {
"active_edge": "Rising",
"init_behavior": "Defined",
"period": 20000,
"reset_kind": "Asynchronous",
"reset_polarity": "ActiveHigh"
}
},
"files": [
{ "name": "top_entity.v", "sha256": "..." },
{ "name": "top_entity.sdc", "sha256": "..." }
],
"top_component": {
"name": "top_entity",
"ports_flat": [
{ "direction": "in", "is_clock": true, "name": "CLK", "width": 1 },
...
]
}
}
Dependency Graph
ClashManifestParser.buildDependencyGraph() recursively follows dependency manifests and returns components in post-order (leaves first, top last). Each component’s dependencies list is reduced to direct only — transitive deps are removed via removeTransitiveDeps to prevent Yosys “Re-definition of module” errors during OOC synthesis.
Domain Analysis
Clock domain periods in the manifest are in picoseconds. The parser converts to MHz:
frequencyMHz = 1_000_000 / periodPs
For example, Dom50 with period: 20000 (20 ns) → 50 MHz. This is the value place
& route is constrained against — see Timing Analysis.
Which domain counts. domains lists every domain the design mentions, so
the parser never chooses one by name. parseManifest walks
top_component.ports_flat, and for every is_clock port pairs it with the domain
that port declares, producing topClocks: TopClock[] — port, domain, period, and
frequency, all as stated. A port with no domain, a domain the manifest does not
define, or a domain without a usable period throws: the manifest contradicts
itself and no substitute would be honest.
pnrTargetClock(manifest) then answers the one question place & route asks —
which single clock to constrain against. Empty topClocks means no target (a
combinational design); several ports sharing one domain is one target; two or more
domains throws, because nextpnr’s --freq applies to the whole design and cannot
satisfy both.
Nextpnr Integration
Supported Families
PNR_FAMILIES in nextpnr-types.ts maps a synthesisTarget to the nextpnr
binary and device list used for place & route. Only targets present in that map
support P&R:
| Target | Executable | Constraints format | Device flag |
|---|---|---|---|
ecp5 | nextpnr-ecp5 | .lpf | --<device> prefix |
ice40 | nextpnr-ice40 | .pcf | --<device> prefix |
gowin | nextpnr-himbaechel | .cst via --vopt | --device <value> |
The Place & Route command picks the family from the configured
synthesisTarget and prompts for a device from that family’s list — it is not
ECP5-only. Targets with no entry above (generic, xilinx, quicklogic,
sf2) can be synthesized but not placed and routed; the command reports this
and stops.
Nexus and MachXO2 are deliberately unsupported. Their nextpnr binaries take different output and constraint flags (
--fasm,--pdc) thatbuildNextpnrArgsdoes not emit, so listing them would be misleading. They can be re-added once argument handling covers them.
ECP5 Devices
| Device | LUTs | Description |
|---|---|---|
25k / um-25k / um5g-25k | 24K | LFE5U-25F / LFE5UM-25F / LFE5UM5G-25F |
45k / um-45k / um5g-45k | 44K | LFE5U-45F / LFE5UM-45F / LFE5UM5G-45F |
85k / um-85k / um5g-85k | 84K | LFE5U-85F / LFE5UM-85F / LFE5UM5G-85F |
Known packages: CABGA256, CABGA381, CABGA554, CABGA756, CSFBGA285,
CSFBGA381, CSFBGA554. Speed grades: 6, 7, 8 (lower is faster).
The package picker is not hard-coded — NextpnrRunner.getValidPackages() probes
the nextpnr binary for the packages the chosen device actually supports, and the
prompt is skipped entirely when it reports none.
iCE40 Devices
| Device | Logic cells | Notes |
|---|---|---|
lp384 | 384 | |
lp1k / hx1k | 1280 | |
lp4k / hx4k / u4k | 3520 | |
lp8k / hx8k | 7680 | |
up3k | 2800 | UltraPlus |
up5k | 5280 | UltraPlus |
Gowin Devices
Gowin goes through nextpnr-himbaechel, which needs both a device string and a
family= value passed via --vopt:
| Device | LUTs | Notes |
|---|---|---|
GW1N-LV1QN48C6/I5 | 1152 | GW1N-1 (QN48) |
GW1N-UV4LQ144C6/I5 | 4608 | GW1N-4 (LQ144) |
GW1N-LV9QN88C6/I5 | 8640 | GW1N-9 / GW1N-9C (QN88) — differ by family= |
GW1NR-LV9QN88PC6/I5 | 8640 | GW1NR-9 / GW1NR-9C, with SDRAM |
GW1NSR-LV4CQN48PC7/I6 | 4608 | GW1NSR-4C (QN48), with SDRAM |
GW2A-LV18QN88C8/I7 | 20736 | GW2A-18 / GW2A-18C |
Command-Line Arguments
NextpnrRunner.buildNextpnrArgs() constructs:
nextpnr-ecp5 \
--json design.json \
--textcfg output.config \
--25k \
--package CABGA381 \
--speed 6 \
--freq 50 \ # top entity's clock domain, when it has one
--lpf constraints.lpf # when provided
Development Setup
Prerequisites
The project uses Nix to provide a reproducible development environment. The flake.nix at the repository root pulls in:
- Node.js 20 + npm + TypeScript
- GHC with Clash and its compiler plugins
- Cabal for building the test Haskell project
- Haskell Language Server (HLS)
- Yosys for logic synthesis
- nextpnr for place & route (ice40, ecp5, …)
- mdbook for building this book
Enter the shell:
nix develop
The dev shell prints the resolved version of each tool on entry, which is the quickest way to confirm the environment is what you expect.
Note that the dev shell provides these for extension development. At runtime the extension does not depend on them being present — it can download its own EDA tools, as described in Getting Started.
Building the Extension
npm install # once
npm run compile # one-off build
npm run watch # incremental recompilation (background)
Running in VS Code
- Open this repository in VS Code.
- Press F5 to launch the Extension Development Host.
- In the new window, open the
test-project/folder. - Open
src/Example/Project.hsand wait for HLS to initialise. - Use the Command Palette (
Ctrl+Shift+P) to invoke Clash commands.
Project Layout
| Path | Purpose |
|---|---|
src/ | Extension source (TypeScript) |
src/test/ | Mocha test suites |
test-project/ | Sample Haskell/Clash project used during development |
book/ | mdbook documentation (this book) |
flake.nix | Nix dev-shell definition |
Building the Documentation
mdbook build book # render to book/book/ (git-ignored)
mdbook serve book # live-reloading preview on localhost:3000
NixOS Notes
npm test works from the terminal, including on NixOS — runTest.ts strips the
environment variables VS Code leaks into its integrated terminal and honours
VSCODE_EXECUTABLE_PATH for hosts where the downloaded Electron binary cannot
find system libraries (libglib-2.0.so.0, etc.). Point that variable at a
nix-wrapped Electron build if the default download fails. See the
Testing chapter for details.
Testing
Test Suites
All tests live under src/test/suite/ and use Mocha in TDD mode (suite / test).
| File | Kind | What it covers |
|---|---|---|
type-analyzer.test.ts | Unit | Monomorphic/polymorphic detection, edge cases |
code-generator.test.ts | Unit | Wrapper generation, port annotations, DiffClock handling |
manifest-frequency.test.ts | Unit | Which clock domain sets the P&R target frequency |
diagram-tabs.test.ts | Unit | Diagrams reuse one preview tab; pinned ones are kept |
gitignore.test.ts | Unit | When the .clash/ gitignore offer is made and what it writes |
run-history.test.ts | Unit | Reading past runs off disk; which directories count as history |
contributions.test.ts | Unit | package.json contributions match what the extension registers |
synthesis-features.test.ts | Unit | Commands, configuration, synthesis types |
synthesis-targets.test.ts | Unit | Target registry, default/resolved scripts, script diffing, and that the installed Yosys supports every offered target |
parallelism.test.ts | Unit | Job-count resolution shared by the cabal/GHC/Yosys/nextpnr *Jobs settings |
settings-panel.test.ts | Unit | The settings panel’s embedded webview script parses and its custom-script keys resolve |
code-actions.test.ts | Unit | Code action provider for Haskell functions |
platform-tools.test.ts | Unit | Yosys/nextpnr tool detection |
tool-provider.test.ts | Unit | Managed toolchain resolution and install paths |
toolchain.test.ts | Unit | Full toolchain availability |
clash-compiler.test.ts | Unit | Clash compiler invocation helpers |
nextpnr-runner.test.ts | Unit | nextpnr child-process lifecycle |
results-tree.test.ts | Unit | Results tree construction |
clash-tree.test.ts | Unit | Sidebar sections and routing to their providers |
functions-tree.test.ts | Unit | The Functions section’s empty states: HLS missing, working, or done |
internal-components.test.ts | Unit | Internal component expansion |
netlist-renderer.test.ts | Unit | netlistsvg-based diagram rendering |
packaging.test.ts | Unit | What vsce would package: deny list, required files |
spawn-options.test.ts | Unit | Every external-tool spawn passes windowsHide, so no console window flashes on Windows |
pnr-targets.test.ts | Integration | End-to-end synthesis + place & route per target |
ooc-blackbox.test.ts | Integration | Out-of-context scripts stub sub-components as black boxes; a real Yosys run proves they survive |
hls-client.test.ts | Integration | HLS communication |
function-detector.test.ts | Integration | Function detection from real Haskell files via HLS |
integration.test.ts | Integration | Per-module synthesis, target frequency, end-to-end flows |
Running Tests
From the terminal
npm test
This compiles first (via pretest) and then launches a headless VS Code
instance against test-project/.
runTest.ts handles the two environment problems that used to make this fail:
- NixOS / headless hosts. Set
VSCODE_EXECUTABLE_PATHto a nix-wrapped Electron binary (e.g. fromvscode-fhs) if the downloaded VS Code build cannot find its system libraries. Leave it unset to let@vscode/test-electrondownload a matching build. It must be a real Electron binary, not thecodeCLI wrapper — the wrapper backgrounds the app and exits 0, so the test host never runs. - Running from VS Code’s integrated terminal. The parent editor leaks
ELECTRON_RUN_AS_NODE=1and a set ofVSCODE_*variables that would make the test host run as plain Node or attach to the running instance.runTest.tsstrips them, so the integrated terminal works the same as an external one.
From VS Code
- Ctrl+Shift+D → select Extension Tests
- Press F5
A second VS Code window opens with the test-project workspace, runs all suites, and reports results in the Debug Console. Use this when you want breakpoints.
Writing a New Test
import * as assert from 'assert';
suite('My Feature', () => {
test('does the right thing', () => {
assert.strictEqual(1 + 1, 2);
});
test('async operation', async function () {
this.timeout(10_000);
const result = await someAsyncCall();
assert.ok(result);
});
});
Place the file in src/test/suite/ with a .test.ts suffix — the test runner picks it up automatically via the glob in index.ts.
Debugging a Test
Set breakpoints in your .test.ts file, then launch Extension Tests with F5. Execution pauses at breakpoints; use the Debug panel to inspect state.
Debugging
Log Channels
| Channel | Where | Content |
|---|---|---|
| Clash Synthesis | Output panel | Extension operations, Clash/Yosys/nextpnr invocations |
| File log | .clash/debug.log in the workspace | Same detail, persisted to disk |
| Extension Host | Output panel | Extension lifecycle events |
| Developer Tools Console | Help → Toggle Developer Tools | Low-level errors, stack traces |
The file log is the one to reach for after a crash: the previous session is
rotated to .clash/debug.log.old on activation, so evidence survives a restart
that would otherwise clear the Output panel.
Attaching the Debugger
- Open the extension project in VS Code.
- Press F5 (launch config: Run Extension).
- Reproduce the problem in the Extension Development Host window.
- When an exception is thrown the debugger breaks at the throw site.
Minimal launch (disable other extensions)
{
"name": "Extension (Minimal)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-extensions"
]
}
Common Crash Causes
| Symptom | Likely cause | Fix |
|---|---|---|
| Crash on function detection | HLS not running | Wait for HLS to initialise; run cabal build first |
| File-system errors | Missing write permission or full disk | Check workspace permissions |
| High CPU then crash | Possible infinite loop (unlikely) | Check htop; file an issue |
| Memory growth | Large synthesis output | Restart VS Code; close heavy extensions |
NixOS-Specific Issues
- Node.js library mismatch: VS Code Server bundles its own Node binary. On NixOS, verify with
ldd ~/.vscode-server/bin/*/node. - Extension Host restarts: Often caused by HLS or other extensions polling. Disable unrelated extensions to isolate.
- Missing shared libraries: Use
nix-ldorvscode-fhsfrom Nixpkgs.
Collecting a Bug Report
- Stack trace from Developer Tools Console
- Last entries from the Clash Synthesis output channel
- Extension Host log
- Steps to reproduce
code --versionoutput
Known Warnings
When running the extension in development mode the Debug Console may show the following messages. All are harmless.
Punycode deprecation
(node:xxxxx) [DEP0040] DeprecationWarning: The `punycode` module is deprecated.
Comes from vscode-languageclient transitive dependencies. Will disappear when upstream packages migrate.
SQLite experimental warning
(node:xxxxx) ExperimentalWarning: SQLite is an experimental feature and might change at any time
Emitted by VS Code’s own internals — not related to this extension.
ApplicationInsights telemetry error
ApplicationInsights:Sender (2) ['Ingestion endpoint could not be reached...
VS Code telemetry failing to reach Microsoft servers. Common in offline or firewalled environments.
Suppressing Warnings
Add to your launch configuration’s env block:
"env": {
"NODE_OPTIONS": "--no-deprecation --no-warnings"
}
Or use the filter button in the Debug Console to hide warnings and show only errors.
Releasing
Publishing a version of the extension to the VS Code Marketplace. Every step is a command you can run locally; nothing here is automated by CI, which builds and lints each push but never publishes.
Before you start
- A clean working tree apart from the changes going into the release.
nix develop(ordirenv allow), which provides node,vsce,mdbook, and the EDA tools the integration tests need.- A Personal Access Token for the
LucasBollenpublisher, from Azure DevOps with the Marketplace → Manage scope. Either export it asVSCE_PATor runnpx vsce login LucasBollenonce. Publishing is the only step that needs it.
1. Write the changelog entry
CHANGELOG.md follows Keep a Changelog.
Rename the ## [Unreleased] heading to the new version and today’s date, and
sort what is under it into Added / Changed / Deprecated / Removed / Fixed, in
that order.
Two things worth the effort, because this file is what users read in the Marketplace’s Changelog tab:
- Say why, not just what. A behaviour that changed is easier to accept when the entry says what was wrong with the old one.
- Give removals their own section. A setting that disappeared, or a renamed
view id, is the entry someone searches for when their configuration stops
working. Say what happens to a value left behind in
settings.json.
2. Pick and set the version
npm version <x.y.z> --no-git-tag-version — this updates package.json and both
version fields in package-lock.json, and the --no-git-tag-version keeps it
from tagging before the work is committed.
The extension is pre-1.0, so:
| Change | Bump |
|---|---|
| Bug fixes only | patch — 0.3.0 → 0.3.1 |
| New features, removed settings, renamed contribution points | minor — 0.3.1 → 0.4.0 |
A published version number can never be reused, so it is worth getting right before the publish step rather than after.
3. Run the test suite
npm run clean && bash scripts/test.sh
scripts/test.sh compiles, lints, then runs the suite in a real VS Code
instance. It must be a clean build: out/ keeps the compiled JavaScript of
deleted test files, and mocha runs whatever it finds there, so a stale artifact
fails a suite that no longer exists. See Testing for what the
suites cover.
4. Check the documentation still describes the extension
The book is the documentation, so a release with stale pages ships stale docs.
mdbook build book # must succeed
A build only proves the Markdown parses. What it cannot check, and you should:
- Settings: every property in
contributes.configurationappears inbook/src/guide/configuration.md, and nothing removed still does. - Commands:
contributes.commandsagainstbook/src/guide/commands.md. - Contribution points: view ids, context values and menu entries against
book/src/guide/sidebar.md. - Test suites:
src/test/suite/*.test.tsagainst the table inbook/src/dev/testing.md.
A quick way to catch the first one:
node -e "
const cfg = require('./package.json').contributes.configuration;
const doc = require('fs').readFileSync('book/src/guide/configuration.md','utf8');
for (const cat of cfg)
for (const key of Object.keys(cat.properties))
if (!doc.includes('\`' + key.replace('clash-toolkit.','') + '\`'))
console.log('undocumented:', key);
"
Grepping the book for the name of anything you deleted is the other half — a removed setting or view id usually appears in more pages than you remember.
5. Verify what will be packaged
npm run verify:package
scripts/verify-package.js runs vsce ls and checks the real file list against
a deny list and a required list. It exists because 0.3.0 shipped
.clash/debug.log, with absolute paths from the machine that built it: vsce
reads only .vscodeignore, never .gitignore, so being untracked is not enough
to keep a file out of the package.
It runs automatically from vscode:prepublish, which gates both vsce package
and vsce publish — but running it directly gives a readable report.
Also confirm package.json still describes the extension: displayName,
description, categories, keywords, icon, repository, license, and
the engines.vscode range you actually test against.
6. Commit
Conventional commits, as the history uses:
feat(sidebar): fold the three views into one
fix(history): resolve each component's Verilog
chore(release): 0.4.0
Keep the version bump and changelog as their own chore(release) commit at the
end, so the release is one commit to tag and revert.
7. Build the package
npx vsce package
Produces clash-toolkit-<version>.vsix. Install it into a real editor before
publishing — the test suite drives the extension host, but it does not look at
the result:
code --install-extension clash-toolkit-<version>.vsix
Then open a Clash project and exercise what changed: the sidebar, a synthesis run, and any command the release touched.
8. Publish
npx vsce publish # or: npx vsce publish --packagePath clash-toolkit-<version>.vsix
This is public and cannot be undone. A version can be unpublished, but its number can never be reused, and anyone who has already installed it keeps it. Publish only from a commit that is in the repository, so what is on the Marketplace can always be traced back to a tree.
Afterwards:
git tag -a v<version> -m "v<version>"
git push && git push --tags
Pushing to master also republishes the book, which the docs workflow builds
from book/ on every push.