@rich-netlist/runtime
v0.4.1
Published
The RNL core as WebAssembly: SPICE and Spectre netlists with their RNL records read into the Documents a viewer draws, the editor interface's snapshots, and edits written back into the netlist.
Maintainers
Readme
@rich-netlist/runtime
The core of RNL, the Rich Netlist, as WebAssembly for the browser and Node. It reads a SPICE or Spectre netlist with its RNL records, the drawing of a schematic written into the netlist, and gives:
- the Document a viewer draws, with an SVG of each level;
- the editor interface's snapshot;
- the netlist with an edit's operations written into it.
It is the same core as the rnl command and the desktop viewer, and gives the Documents they give. It bundles only the standard packages, the series 0.2 and 0.3 (including rnl-zh). Other packages must be supplied explicitly. Starting with the next npm version after 0.1.0-preview.14, optional package files are distributed by the public package catalog, rather than the npm tarball. Published preview.14 is unchanged.
Install
Core releases use the workspace version and core-vX.Y.Z, shared with the CLI. Install an exact published version:
npm i @rich-netlist/[email protected] # replace X.Y.Z with the chosen published core versionUse
startCore runs the core in a module Worker, or in a worker thread in Node:
import { startCore } from "@rich-netlist/runtime";
const core = startCore();
const files = { "/top.sp": netlistText };
const document = await core.build({ entry: "/top.sp", files });
// the snapshot takes the parts to give: the host circuit and the model of each level here
const snapshot = await core.snapshot({ entry: "/top.sp", files, parts: { host: true, model: true } });
core.terminate();@rich-netlist/runtime/core loads the module in the calling thread instead, and @rich-netlist/runtime/worker is the Worker itself. The requests, the Document, the snapshot and the catalog are declared in the package's TypeScript files. The binding's README describes the requests, collect for a frontend's own file access, and decodeText.
Inspection overlays
Every Document gives levels[].usages[].frame (bounds, title and role anchors/widths)
and levels[].currents (terminal, direction, pin and arrow points), in SVG pixels
with y down. These geometry records contain no colors, strokes or visibility settings.
The SVG has no inspection overlays unless requested:
const document = await core.build({ entry: "/top.sp", files, overlays: {
usages: "semantic", // "none" (default), "semantic" or "all"
currents: true, // false by default; unspecified currents produce no arrow
roles: true, // false by default; labels on enabled usage frames
style: { palette: ["#4263eb", "#2f9e44"], width: 2, dashes: [1, 5], current_color: "#d9480f" }
} });Styles are independent of RNL @SET-STYLE; omitted properties use deterministic renderer
defaults. Colors are hex (#RGB, #RRGGBB, #RRGGBBAA), the palette is nonempty,
width is positive, and dash lengths are positive (an empty array means solid).
Palette assignment follows usage order, including hidden usages. Current arrows are solid.
The same options work with lazy builds and a snapshot's drawing. Disabling overlays returns
the original SVG bytes. Viewers may render the geometry with their own styling/preferences.
Declaration currents (#:port) use explicitly drawn $IO_PORT usages on the port's net;
in points from the tag into the circuit. A port with no such symbol has no arrow.
Optional Canvas package
Fetch the exact package record from the public catalog, verify its raw bytes, and explicitly add the decoded text to files and its path to pool:
import { startCore } from "@rich-netlist/runtime";
const catalogUrl = "https://rich-netlist.github.io/rnl-packages/catalog.json";
const response = await fetch(catalogUrl);
if (!response.ok) throw new Error(`Catalog HTTP ${response.status}`);
const catalog = await response.json();
if (catalog.schemaVersion !== 1) throw new Error("Unsupported catalog");
const pkg = catalog.packages.find(p => p.name === "analog-canvas-extensions" && p.version === "0.1");
if (!pkg) throw new Error("Missing analog-canvas-extensions 0.1");
const download = await fetch(new URL(pkg.url, catalogUrl));
if (!download.ok) throw new Error(`Package HTTP ${download.status}`);
const bytes = await download.arrayBuffer();
const hash = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
b => b.toString(16).padStart(2, "0")).join("");
if (hash !== pkg.sha256) throw new Error("Public package SHA-256 mismatch");
const core = startCore();
const packagePath = "/library/analog-canvas-extensions.rnl";
const files = { "/top.sp": netlistText, [packagePath]: new TextDecoder().decode(bytes) };
const pool = [packagePath];
const document = await core.build({ entry: "/top.sp", files, pool });The netlist still writes @USE-PACKAGE(name="analog-canvas-extensions", version="0.1").
Keep the same files and pool for snapshot requests and subsequent rebuilds. Supplying only
files does not make a package searchable. Applications that need repeatable builds should
pin the expected SHA-256 as well as the package name and version. This repository's smoke
fixture uses its single packages.lock.json via python scripts/packages.py download.
Catalog
catalog lists the symbols, definitions and sets of the packages the core has: the bundled ones and a request's own, those of its package_dirs among its files. Each package is read as the core reads it, so a symbol's pins and box are those of the drawings:
const catalog = await core.catalog({ catalog: {} });
const resistor = catalog.symbols.find((s) => s.symbol === "$RESISTOR");
// resistor.package is "rnl-std-basic", resistor.version "0.3", resistor.box [-1, -2.4, 1, 2.4], and
// resistor.pins [{ terminal: 1, at: [0, 2], direction: "up" }, { terminal: 2, at: [0, -2], direction: "down" }]Of each name only the newest version is listed; { catalog: { packages: ["rnl-std-basic"], all_versions: true } } keeps the packages of these names, every version of each. A symbol's definition names the definition whose usages a rule of the packages draws with it, such as $GROUND for $GROUND_SYMBOL.
Objects without a position
A netlist that has no positions yet lists every object in Level.unplaced, each with its symbol drawn alone as svg, about 1.3 KB each. A frontend that shows a few of them at a time builds with lazy_unplaced: true, which leaves those drawings out of the Document and keeps them in the core, and asks for the drawings of the objects it shows, by their places in the level's list and their names, which the core checks against the build it kept:
const document = await core.build({ entry: "/top.sp", files, lazy_unplaced: true });
const level = 0;
const unplaced = document.levels[level].unplaced.slice(0, 20);
const drawings = await core.drawings({
drawings: { level, items: unplaced.map((_, i) => i), names: unplaced.map((u) => u.name) },
});
// drawings[0].svg draws unplaced[0]The request is about the last build that had lazy_unplaced, and is refused, with a message, when no build did, when an object is not there, or when the names are those of another build.
Manifests
@rich-netlist/runtime/manifest reads the files a manifest (format 1) lists, as the web viewer's ?manifest= does, such as the manifests the RNL gallery writes: it fetches the files, checks each against the SHA-256 and size of its bytes, and gives what a build takes. The same format is a viewer project's rnl-project.json (PROJECT_MANIFEST), in a ZIP archive with its files, and a manifest of links.
import { startCore } from "@rich-netlist/runtime";
import { readManifest } from "@rich-netlist/runtime/manifest";
const design = await readManifest("https://example.org/manifests/amp.json", { onProgress: (done, total) => {} });
const core = startCore();
const document = await core.build({ entry: design.entry, files: design.files, pool: design.pool, formats: design.formats });files holds the text of every file by its path under /<id>/ (or the root given), bytes their bytes, pool the manifest's pool in its order, formats the parse format chosen for some files by the same paths, and level the level the files open on, a subcircuit's name or "(top)". Files are fetched from their own url or from the manifest's base joined with their path, https addresses (http only on this computer), and their bytes checked before they are decoded. A file that does not match or cannot be fetched rejects with a ManifestError whose file names it. A fetched manifest's files are at most 300, 2 MB each and 20 MB in all.
loadManifest(json, { readFile }) reads a manifest already parsed, such as a project's, with readFile(path) giving each file's bytes from its archive; writeManifest({ files, entry, pool, formats, level, archive }) writes one, with the SHA-256 and size of each file's bytes, and checks it as a reader would. Node 18 and later, and browsers, are supported.
Vite
A frontend built with Vite needs three settings:
worker: { format: "es" }: the Worker is a module with a top-levelawait, which Vite's default format for Workers,"iife", cannot bundle.optimizeDeps: { exclude: ["@rich-netlist/runtime"] }: the dev server's pre-bundling moves the modules, andnew URL("./worker.js", import.meta.url)then names a file that is not there.server.fs.allowwith the package's real directory, but only when the package is linked from outside the frontend's workspace, aslink:does: the dev server otherwise refuses to serveworker.js. A package installed intonode_modulesdoes not need it.
Vite may say that node:worker_threads and node:fs/promises were left out for the browser: the modules import them only when they run in Node.
Versions
The npm package and CLI share the root workspace version, released together by core-vX.Y.Z.
Viewer and language-standard versions are independent. Versions 0.x may change requests,
Documents and snapshots. Historical runtime-v0.1.0-preview.N tags published npm-only
previews (through preview.15); that trigger is retired and those versions remain unchanged.
The first core version and npm dist-tag (next or latest) are pending the user's decision.
A bare install follows npm's latest, which may still point to an older preview; use an exact
version until the release decision is applied. See release procedures
for CI, trusted publishing and partial-publication recovery.
License
The JavaScript modules and the code compiled into the module are under the MIT license (LICENSE). The package also holds the built-in standard packages under CC0-1.0, and the metrics of the font Noto Sans SC, copyright 2014-2021 Adobe, under the SIL Open Font License 1.1 (LICENSES/).
