Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ToolPurpose
Cabal (drives cabal run clash-synth:clash --)Builds your project and invokes Clash to generate Verilog
Haskell Language ServerFunction 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.

ToolPurposeNeeded for
YosysLogic synthesis and statisticsElaborate, Synthesize, Place & Route
nextpnr-ecp5Place & route for Lattice ECP5Place & Route, ecp5 target
nextpnr-ice40Place & route for Lattice iCE40Place & Route, ice40 target
nextpnr-himbaechelPlace & route for GowinPlace & 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

  1. Open a Clash project in VS Code (one that builds with cabal build).
  2. Make sure HLS is running (install the Haskell extension).
  3. Open a .hs file containing monomorphic functions.
  4. 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

  1. Function detection — HLS provides document symbols and hover types. The type analyser checks whether a function is monomorphic (all concrete types, no type variables).

  2. Wrapper generation — For a function like topEntity in module Example.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 DiffClock are handled automatically with PortProduct annotations.

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

  4. Clash compilation — Runs cabal run clash-synth:clash -- ClashSynth_TopEntity --verilog inside the synth project.

  5. Yosys synthesis — Runs Yosys with a script chosen by the synthesisTarget setting (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.

  6. 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:

  1. If you opted to have that tool managed and a managed copy is on disk, the managed absolute path is used.
  2. 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:

ToolUsed for
YosysElaborate, Synthesize, Place & Route
nextpnr-ecp5Place & route for Lattice ECP5
nextpnr-ice40Place & route for Lattice iCE40
nextpnr-himbaechelPlace & 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:

PlatformApproximate download
Linux x64730 MB
Linux arm64620 MB
macOS arm64510 MB
macOS x64485 MB
Windows x64335 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:

RowMeaningWhat to do
Analyzing…The extension is analysing symbols HLS returnedWait; it’s quick
No symbols from HLS yetHLS is reachable but returned nothing for this fileUsually 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 installedFunction detection needs haskell.haskellClick the row to open it in the Marketplace
HLS unavailable — Haskell extension did not startIt is installed but did not activateClick the row to retry; its own output channel says why
Open a Haskell file to see functionsNo Haskell file is activeOpen one
Monomorphic (0) / Polymorphic (0)HLS answered, and this file defines no top-level functionsNothing — 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:

SectionContents
TimingMax frequency, pre-route estimate (when it differs), critical-path delay, setup and hold slack, and whether constraints were MET or MISSED
UtilizationLUTs, registers, BRAM, DSP, IO — each as used / total with a percentage
Critical PathsEach 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.json in the run’s 04-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

CommandDescription
Clash: Detect FunctionsScan the current file for functions, show which are synthesisable
Clash: Generate VerilogGenerate a wrapper module and compile to Verilog with Clash
Clash: ElaborateClash → Yosys elaboration only (no tech mapping), one diagram per module
Clash: SynthesizeFull Clash → Yosys pipeline without place & route. Optional out-of-context mode for per-module diagrams
Clash: Place & RouteFull pipeline: Clash → Yosys → nextpnr

Toolchain

CommandDescription
Clash: Check ToolchainProbe every external tool (cabal, Yosys, nextpnr) and report what is reachable
Clash: Install ToolchainPick 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
CommandWhereDescription
Clash: RefreshSidebar title barRe-read the active file’s functions and the runs on disk
Clash: Refresh Haskell FunctionsPaletteRe-scan the active file for functions only
Clash: Go To FunctionClick a Functions itemJump to the function’s definition
Clash: Open SettingsGear icon in the title barOpen the settings panel (synthesis target and scripts)
Clash: Open Synthesized VerilogResults item, inline iconOpen the Yosys-synthesized Verilog
Clash: View Module DiagramResults item, inline iconOpen a module’s schematic SVG
Clash: View Component DiagramClick a sub-component row under a moduleOpen that component’s own schematic (rendered on first open)
Clash: Refresh Run HistoryPaletteRe-read past runs from .clash/ only
Clash: Show Run in Synthesis ResultsClick a History itemLoad a past run into the Results section
Clash: Open Verilog (History)History item, inline iconOpen a past run’s Verilog
Clash: View Diagram (History)History item, inline iconOpen a past run’s diagram
Clash: Delete RunHistory run row, inline iconDelete that run’s output directory
Clash: Delete Design HistoryHistory design row, inline iconDelete every run recorded for that design
Clash: Clear Run HistoryTrash icon on the History headerDelete 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:

  1. Detects functions in the current file
  2. Shows a picker with only synthesizable (monomorphic) functions
  3. Generates a Clash wrapper module
  4. Compiles to Verilog with Clash
  5. 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:

  1. Generates wrapper module
  2. Compiles to Verilog with Clash
  3. Synthesizes with Yosys for the configured target
  4. Reads the top entity’s target clock frequency from the Clash manifest
  5. 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.

SettingDefaultDescription
synthesisTargetgenericTarget FPGA family for Yosys synthesis. One of generic, ice40, ecp5, xilinx, gowin, quicklogic, sf2. Also selects the nextpnr binary for Place & Route
outOfContextfalseOut-of-context synthesis: when enabled, each component in a multi-component design is synthesized standalone with its own diagram + utilization stats
pnrWriteRoutedSvgtrueWrite a routed-layout SVG alongside the nextpnr output, showing where the design landed on the fabric
cabalJobsautoPackages 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
yosysJobsautoComponents Yosys may synthesize at once on the per-component path. auto is one process per core less one, capped at 8
nextpnrThreadsautoThreads 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.

yosysCommand was the single-tool ancestor of this setting. It is deprecated but still honoured; move it to toolCommands under the key yosys.

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:

PlaceholderExpands 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 elaborationScript has 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 .json netlist per component
  • An .svg circuit 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’s LUT4 / TRELLIS_FF / block RAMs. On the test design, whole-design ecp5 synthesis reports 173 cells (CCU2C, LUT4, TRELLIS_FF) while the same design’s components report generic cells. The script this path does use is outOfContextScript, 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 synth per component hangs indefinitely on components containing large block RAMs, because memory_map plus abc cannot finish on the resulting flip-flop array. Keeping memories as $mem cells 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.

SettingUnit of workauto capWhy the cap
cabalJobspackages(cabal decides)passed through as cabal’s own $ncpus
ghcJobsmodules in a package(off by default)see above
yosysJobswhole components8past that the runs contend for memory, not CPU — each holds a whole design
nextpnrThreadsnextpnr’s threaded passes4only 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.

nextpnrThreads interacts with reproducibility: the passes nextpnr threads are the ones whose result can depend on scheduling, so a fixed --seed only pins a run’s outcome at a fixed thread count. Set nextpnrThreads to 1 if 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 only nextpnr-ecp5 does 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 outOfContext toggle (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 -json line, synthesis fails loudly rather than silently reporting no statistics — the extension treats a missing stats.json as 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 outOfContext enabled, 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

MetricMeaning
Pre-Routing FrequencyEstimated FMax before routing — optimistic upper bound
Max FrequencyActual FMax after routing — the real achievable clock speed
Critical Path DelayThe longest combinational path in nanoseconds
Constraints MetWhether 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 saysWhat happens
One clock domain across the top entity’s clock portsThat domain’s period is the target
No clock ports at allNo --freq — nextpnr reports an unconstrained Fmax, which is what a combinational design has
Clock ports in two or more domainsPlace & 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 periodThe 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):

  1. Create the “Clash Synthesis” output channel
  2. Initialize the file logger at .clash/debug.log
  3. Instantiate the pipeline components (HLSClient, FunctionDetector, CodeGenerator, ClashCompiler, YosysRunner, NextpnrRunner) and the clash diagnostic collection
  4. Initialize the managed tool provider, then the ToolchainChecker
  5. 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, since SubComponentItem rows can come from two of them). It is registered with createTreeView rather than registerTreeDataProvider for its .selection property, which title-bar buttons read to find the selected function
  6. Subscribe to active-editor changes so the functions view follows the current Haskell file
  7. Register commands and the code action provider for Haskell files
  8. 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 patternPort annotation
Clock …PortName "CLK"
DiffClock …PortProduct "CLK" [PortName "p", PortName "n"]
Reset …PortName "RST"
Enable …PortName "EN"
Anything elsePortName "INA", PortName "INB", …
OutputPortName "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:

  1. The component’s direct dependencies are read with read_verilog -lib, which keeps their port interfaces and discards their bodies. They become black boxes, so hierarchy -check passes 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
  2. The component is optimized standalone — proc, opt -purge, memory -nomap, opt, with no flatten. There is also no technology mapping: a full synth per component hangs on components with large block RAMs (memory_map + abc on the resulting flip-flop array), so the target’s synth_* command and any custom script are not used on this path
  3. Black-box instances get setattr -set keep 1 t:<dep> right after proc. Without it, opt/opt_clean/clean delete any instance whose outputs happen to be unused, and the component would silently vanish from the diagram and the cell counts
  4. 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
  5. The component graph is written to per-module/hierarchy.json so 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.js run 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 .gitignoreYes 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:

TargetExecutableConstraints formatDevice flag
ecp5nextpnr-ecp5.lpf--<device> prefix
ice40nextpnr-ice40.pcf--<device> prefix
gowinnextpnr-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) that buildNextpnrArgs does not emit, so listing them would be misleading. They can be re-added once argument handling covers them.

ECP5 Devices

DeviceLUTsDescription
25k / um-25k / um5g-25k24KLFE5U-25F / LFE5UM-25F / LFE5UM5G-25F
45k / um-45k / um5g-45k44KLFE5U-45F / LFE5UM-45F / LFE5UM5G-45F
85k / um-85k / um5g-85k84KLFE5U-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

DeviceLogic cellsNotes
lp384384
lp1k / hx1k1280
lp4k / hx4k / u4k3520
lp8k / hx8k7680
up3k2800UltraPlus
up5k5280UltraPlus

Gowin Devices

Gowin goes through nextpnr-himbaechel, which needs both a device string and a family= value passed via --vopt:

DeviceLUTsNotes
GW1N-LV1QN48C6/I51152GW1N-1 (QN48)
GW1N-UV4LQ144C6/I54608GW1N-4 (LQ144)
GW1N-LV9QN88C6/I58640GW1N-9 / GW1N-9C (QN88) — differ by family=
GW1NR-LV9QN88PC6/I58640GW1NR-9 / GW1NR-9C, with SDRAM
GW1NSR-LV4CQN48PC7/I64608GW1NSR-4C (QN48), with SDRAM
GW2A-LV18QN88C8/I720736GW2A-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

  1. Open this repository in VS Code.
  2. Press F5 to launch the Extension Development Host.
  3. In the new window, open the test-project/ folder.
  4. Open src/Example/Project.hs and wait for HLS to initialise.
  5. Use the Command Palette (Ctrl+Shift+P) to invoke Clash commands.

Project Layout

PathPurpose
src/Extension source (TypeScript)
src/test/Mocha test suites
test-project/Sample Haskell/Clash project used during development
book/mdbook documentation (this book)
flake.nixNix 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).

FileKindWhat it covers
type-analyzer.test.tsUnitMonomorphic/polymorphic detection, edge cases
code-generator.test.tsUnitWrapper generation, port annotations, DiffClock handling
manifest-frequency.test.tsUnitWhich clock domain sets the P&R target frequency
diagram-tabs.test.tsUnitDiagrams reuse one preview tab; pinned ones are kept
gitignore.test.tsUnitWhen the .clash/ gitignore offer is made and what it writes
run-history.test.tsUnitReading past runs off disk; which directories count as history
contributions.test.tsUnitpackage.json contributions match what the extension registers
synthesis-features.test.tsUnitCommands, configuration, synthesis types
synthesis-targets.test.tsUnitTarget registry, default/resolved scripts, script diffing, and that the installed Yosys supports every offered target
parallelism.test.tsUnitJob-count resolution shared by the cabal/GHC/Yosys/nextpnr *Jobs settings
settings-panel.test.tsUnitThe settings panel’s embedded webview script parses and its custom-script keys resolve
code-actions.test.tsUnitCode action provider for Haskell functions
platform-tools.test.tsUnitYosys/nextpnr tool detection
tool-provider.test.tsUnitManaged toolchain resolution and install paths
toolchain.test.tsUnitFull toolchain availability
clash-compiler.test.tsUnitClash compiler invocation helpers
nextpnr-runner.test.tsUnitnextpnr child-process lifecycle
results-tree.test.tsUnitResults tree construction
clash-tree.test.tsUnitSidebar sections and routing to their providers
functions-tree.test.tsUnitThe Functions section’s empty states: HLS missing, working, or done
internal-components.test.tsUnitInternal component expansion
netlist-renderer.test.tsUnitnetlistsvg-based diagram rendering
packaging.test.tsUnitWhat vsce would package: deny list, required files
spawn-options.test.tsUnitEvery external-tool spawn passes windowsHide, so no console window flashes on Windows
pnr-targets.test.tsIntegrationEnd-to-end synthesis + place & route per target
ooc-blackbox.test.tsIntegrationOut-of-context scripts stub sub-components as black boxes; a real Yosys run proves they survive
hls-client.test.tsIntegrationHLS communication
function-detector.test.tsIntegrationFunction detection from real Haskell files via HLS
integration.test.tsIntegrationPer-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_PATH to a nix-wrapped Electron binary (e.g. from vscode-fhs) if the downloaded VS Code build cannot find its system libraries. Leave it unset to let @vscode/test-electron download a matching build. It must be a real Electron binary, not the code CLI 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=1 and a set of VSCODE_* variables that would make the test host run as plain Node or attach to the running instance. runTest.ts strips them, so the integrated terminal works the same as an external one.

From VS Code

  1. Ctrl+Shift+D → select Extension Tests
  2. 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

ChannelWhereContent
Clash SynthesisOutput panelExtension operations, Clash/Yosys/nextpnr invocations
File log.clash/debug.log in the workspaceSame detail, persisted to disk
Extension HostOutput panelExtension lifecycle events
Developer Tools ConsoleHelp → Toggle Developer ToolsLow-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

  1. Open the extension project in VS Code.
  2. Press F5 (launch config: Run Extension).
  3. Reproduce the problem in the Extension Development Host window.
  4. 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

SymptomLikely causeFix
Crash on function detectionHLS not runningWait for HLS to initialise; run cabal build first
File-system errorsMissing write permission or full diskCheck workspace permissions
High CPU then crashPossible infinite loop (unlikely)Check htop; file an issue
Memory growthLarge synthesis outputRestart 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-ld or vscode-fhs from Nixpkgs.

Collecting a Bug Report

  1. Stack trace from Developer Tools Console
  2. Last entries from the Clash Synthesis output channel
  3. Extension Host log
  4. Steps to reproduce
  5. code --version output

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 (or direnv allow), which provides node, vsce, mdbook, and the EDA tools the integration tests need.
  • A Personal Access Token for the LucasBollen publisher, from Azure DevOps with the Marketplace → Manage scope. Either export it as VSCE_PAT or run npx vsce login LucasBollen once. 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:

ChangeBump
Bug fixes onlypatch — 0.3.00.3.1
New features, removed settings, renamed contribution pointsminor — 0.3.10.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.configuration appears in book/src/guide/configuration.md, and nothing removed still does.
  • Commands: contributes.commands against book/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.ts against the table in book/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.