Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ jobs:
test:
needs: build
runs-on: ${{ matrix.os }}
# A request that errors leaves some tests waiting for a reply: fail, don't hang.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest ]
python-version: [ "3.10", "3.13" ]
# Oldest najaeda the package declares support for, and the newest.
najaeda: [ "==0.7.29", "" ]
najaeda: [ "==0.7.34", "" ]
steps:
- name: Checkout code
uses: actions/checkout@v5
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ imgui.ini
# Private, local-only working notes (never shared/committed).
notes/
naja_sv_diagnostics.log
naja_vhdl_diagnostics.log

# Viewer bundle: built by CI (-DNAJA_SCHEMATIC_WASM_MODULE=ON), shipped in the wheel.
python/naja_schematic/static/naja-schematic.js
Expand Down
65 changes: 44 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,17 @@ backend is chosen per-platform in `CMakeLists.txt`: `NativeFileDialog.mm`
Windows has no backend and isn't a supported target.

Run directly, optionally with a netlist to load (dispatched by extension —
`.v` → Verilog, `.sv` → SystemVerilog via slang, otherwise treated as an SNL
directory), `--liberty <path>` (repeatable, once per file — defines the
primitive cell library for a `.v` design; not supported for `.sv`, since
`LocalSNLProvider::loadSystemVerilog()`/`SNLSVConstructor` has no liberty
hook, and the CLI fails loudly rather than silently ignoring it), a
`.v` → Verilog, `.sv` → SystemVerilog via slang, `.vhd`/`.vhdl` → VHDL via
naja's experimental frontend (`LocalSNLProvider::loadVHDL()`,
`VHDLConstructor::constructFiles()`; the only kind that takes several
positional files, in any order -- naja resolves the units across them),
otherwise treated as an SNL directory), `--top <name>` (`.sv`/VHDL only:
forces the top module/entity instead of inferring it), `--liberty <path>`
(repeatable, once per file — defines the
primitive cell library for a `.v` design; not supported for `.sv` or VHDL,
since `LocalSNLProvider::loadSystemVerilog()`/`SNLSVConstructor` and
`loadVHDL()` have no liberty hook, and the CLI fails loudly rather than
silently ignoring it), a
diagnosis JSON to pre-load (`--diagnosis <path>` — see
`loadDiagnosisFile()` in `AppLogic.cpp`), and/or directories to look up
relative source file references in (`--source-root <dir>`, repeatable; see
Expand Down Expand Up @@ -169,14 +175,19 @@ python3 scripts/najaeda_server.py --systemverilog a.sv b.sv --top top -D SYNTHES
python3 scripts/najaeda_server.py --flist design.f --top top
```

A design is required: either `--verilog`, or SystemVerilog via
A design is required, exactly one of: `--verilog`; SystemVerilog via
`--systemverilog`/`--sv <path>...` and/or `--flist`/`-f <file>` (the two SV
inputs can be combined; `--flist` maps to `SystemVerilogConfig.flist`).
Verilog and SV are mutually exclusive. `--top`/`--define`/`-D` apply only to
SystemVerilog, and `--liberty` is rejected with SV — same restriction as the
native CLI, since the SV loader has no liberty hook.
`--allow_unknown_designs` maps to `SystemVerilogConfig.blackbox_unknown_modules`
for SV.
inputs can be combined; `--flist` is the raw loader's `flist`); or
VHDL via `--vhdl <path>...` (`NLDB.loadVHDL()`, files in any order).
`load_design()` uses najaeda's raw `naja` API only, like `protocol.py`
(`NLDB.loadGateLevelVerilog()`/`loadSystemVerilog()`/`loadVHDL()`/
`loadLibertyPrimitives()`); `--top` reaches slang as
`frontend_options={"slang": ["--top", name]}` (najaeda >= 0.7.34).
`--top` applies to SystemVerilog and VHDL, `--define`/`-D` only to
SystemVerilog, and `--liberty`/`--yosys-src` are rejected with SV and VHDL —
same restriction as the native CLI, since those loaders have no liberty
hook.
`--allow_unknown_designs` maps to the loaders' `blackbox_unknown_modules`.

Note the flag shapes are *not* symmetric with the native CLI above: the
server takes `--verilog <path>` (singular) + `--liberty <path>...` (one flag,
Expand Down Expand Up @@ -210,7 +221,7 @@ WASM viewer, so najaeda users get a viewer from `pip install` alone:
`--open`, `--diagnosis <json>`, `--stdio`, `--source-root <dir>`
(repeatable; `source_roots=` on `ViewerSession`/`ViewerServer`/`show()`),
`--source-map FROM=TO` (repeatable; `source_maps=` there too),
`--yosys-src` (with `--verilog`: `VerilogConfig.yosys_source_attributes`))
`--yosys-src` (with `--verilog`: `loadGateLevelVerilog(yosys_source_attributes=)`))
and its two transports: a
WebSocket server that also serves the viewer page on the same port
(`static/index.html` sets `Module.najaWsUrl` from `location`, query
Expand Down Expand Up @@ -485,14 +496,24 @@ occurrence's enclosing modules.
Instance entries (`instances_response`/`primitives_response` children,
equipotential occurrences, `instance_resolved`'s `instance`) carry a
`primitive_type` (`"and"`, `"nand"`, `"or"`, `"nor"`, `"xor"`, `"xnor"`,
`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"tie0"`, `"tie1"`, `"unknown"`) that picks the gate
`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"tie0"`, `"tie1"`, `"mux"`,
`"add"`, `"sub"`, `"mul"`, `"shl"`, `"shr"`, `"ashr"`, `"divmod"`,
`"unknown"`) that picks the gate
symbol `SchematicView::drawInstance()` draws, and term entries carry
`"clock": true` on a sequential cell's clock pin (the DFF clock notch).
Both come from naja's modeling of the cell -- `SNLDesignModeling`'s
truth-table checks (`isAnd`, ..., `isConst0`/`isConst1` for tie cells, whose
Liberty `function` is `"0"`/`"1"`), `isSequential` and `isClock` -- and
Liberty `function` is `"0"`/`"1"`; `isMux`, at any width, since a bus mux
is one NLDB0 mux2), `isSequential` and `isClock` -- and
**never from a cell or pin name**: a cell naja has no model for (e.g.
gate-level Verilog without Liberty) is `"unknown"` and draws as a box.
gate-level Verilog without Liberty) is `"unknown"` and draws as a box. A
mux is a trapezoid (`SchematicLayout::muxSlant()`), the word-level
operators SystemVerilog lowers to (`NLDB0::isAddSub()`/`isSubtract()`,
`isMultiply()`, `isShift()` + `getShiftSignature()`, `isDivMod()`) are
boxes with a circled operator glyph (`isOperatorSymbol()`,
`kOperatorGlyphW`). `protocol.py` uses najaeda's bindings of the same
checks (`SNLDesign.isAddSub()`, `isSubtract()`, `isMultiply()`,
`isShift()`, `isShiftRight()`, `isShiftArithmetic()`, `isDivMod()`).

`diagnosis_response` is different: it's a **server push**, not a reply to a
request (a diagnosis run finishes on its own schedule), and it *annotates*
Expand Down Expand Up @@ -524,15 +545,16 @@ all, so it gets diagnosis data via **File > Load Diagnosis JSON...**
`DiagnosisItem` parser) instead. An item with `id_path` matches that
instance only; one with only `path` matches by names, and never through
an anonymous (`""`) level. In Python, an item's `path` may also be a
najaeda `netlist.Instance`, turned into both by `diagnosis_response()`.
naja `SNLPath` or a najaeda `netlist.Instance` (read through `pathIDs`,
without calling `netlist`), turned into both by `diagnosis_response()`.

Getting to one hierarchical instance works in both directions, keyed by
the same `id_path`/`path` pair as diagnosis items:

- **Host → viewer.** `focus_instance` is a server push,
`{"response":"focus_instance","path":["u1","u2"],"id_path":[3,7]}`, from
`Schematic.show_instance()` / `show(instance=...)` (which take a najaeda
`Instance`, a list of ids, or a list of names), re-pushed after each
`Schematic.show_instance()` / `show(instance=...)` (which take a naja
`SNLPath` or najaeda `Instance`, a list of ids, or a list of names), re-pushed after each
`root_response` like diagnoses. The viewer answers it with a
`resolve_instance` request carrying the same `path`/`id_path`,
which both providers implement (`protocol.py`
Expand Down Expand Up @@ -561,7 +583,8 @@ the same `id_path`/`path` pair as diagnosis items:
`get_properties` for it. The notebook widget intercepts
`instance_selected` into `Schematic.selected_id_path` (and
`selected_path`, the names); `Schematic.selected` returns it as a
najaeda `netlist.Instance`, and `on_select()` gives a callback (it
naja `SNLPath` (raw API, like the whole package; empty for the top),
and `on_select()` gives a callback (it
observes the ids, so moving between anonymous siblings fires it). The other hosts only log it (`protocol.py`) or ignore it
(`LocalSNLProvider`).

Expand Down Expand Up @@ -663,7 +686,7 @@ offer a schematic-side "Show Properties" entry point (only terms/instances
appear as boxes/pins there).

**Source references** (full design: `docs/source_information.md`; needs
najaeda >= 0.7.29). naja keeps, per object, an optional *declaration*
najaeda >= 0.7.29; VHDL fills declarations since 0.7.32). naja keeps, per object, an optional *declaration*
(where it's written in the loaded input: a gate's netlist line, an
instance's instantiation, a design's definition) and ordered *origins*
(upstream RTL, e.g. Yosys `src`), each qualified by provider/language/
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ else()
naja_snl_verilog
naja_snl_liberty
naja_snl_systemverilog
naja_snl_vhdl
naja_nl_dump
${PLATFORM_FILE_DIALOG_LIBS}
)
Expand Down Expand Up @@ -296,6 +297,7 @@ if(NOT EMSCRIPTEN_BUILD)
naja_snl_verilog
naja_snl_liberty
naja_snl_systemverilog
naja_snl_vhdl
naja_nl_dump
)
target_compile_definitions(naja-schematic-provider-tests PRIVATE
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ from najaeda import netlist
import naja_schematic

netlist.load_liberty("cells.lib")
netlist.load_verilog("design.v")
netlist.load_gate_level_verilog("design.v")
naja_schematic.show() # the viewer, in the cell output
```

Expand Down Expand Up @@ -71,8 +71,9 @@ your own tool).
nets from a JSON report, e.g. from a formal equivalence check.
- **SVG export** of the schematic, from the File menu or headless from the
command line.
- Loads gate-level Verilog (with Liberty), SystemVerilog, or naja SNL
netlists.
- Loads gate-level Verilog (with Liberty), SystemVerilog, VHDL
(experimental) or naja SNL netlists. Muxes are drawn as muxes, and the
adders, multipliers and shifters SystemVerilog becomes as operator boxes.

## Command-line options

Expand All @@ -82,8 +83,9 @@ plus options, in any order. Their flags differ slightly:
| Desktop app (`naja-schematic-standalone`) | Browser server (`naja-schematic`) | What it does |
|---|---|---|
| `design.v` | `--verilog design.v` | Load a gate-level Verilog netlist |
| `design.sv` | `--sv a.sv b.sv --top top` | Load SystemVerilog (elaborated with slang) |
| `design.sv --top top` | `--sv a.sv b.sv --top top` | Load SystemVerilog (elaborated with slang) |
| | `--flist design.f`, `-D NAME=VALUE` | SystemVerilog command file, defines |
| `a.vhd b.vhd --top top` | `--vhdl a.vhd b.vhd --top top` | Load VHDL, files in any order (naja's experimental frontend) |
| `snl_dir/` | | Load a naja-if (SNL) netlist directory |
| `--liberty a.lib --liberty b.lib` | `--liberty a.lib b.lib` | Liberty cell libraries (Verilog only) |
| `--diagnosis report.json` | `--diagnosis report.json` | Show a diagnosis report on the design |
Expand All @@ -93,7 +95,7 @@ plus options, in any order. Their flags differ slightly:
| | `--open` | Open the viewer in a browser |
| | `--port N`, `--host H` | Where to serve (default `localhost:8081`) |

Without a design, the desktop app starts empty: use the **File** menu (Open Verilog..., Open SystemVerilog..., Open naja-if...). A
Without a design, the desktop app starts empty: use the **File** menu (Open Verilog..., Open SystemVerilog..., Open VHDL..., Open naja-if...). A
diagnosis report can also be loaded later with **File > Load Diagnosis
JSON...**. `naja-schematic --help` lists every server option.

Expand Down
4 changes: 4 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,12 @@ path = [
"tests/VerilogLanguageTest.cpp",
"tests/data/anonymous_protocol_cases.json",
"tests/data/source_protocol_cases.json",
"tests/data/sv/operators.sv",
"tests/data/sv/two_tops.sv",
"tests/data/sources/gate/alu_syn.v",
"tests/data/sources/rtl/alu.v",
"tests/data/vhdl/leaf.vhd",
"tests/data/vhdl/top.vhd",
"tests/provider/LocalSNLProviderProtocolTest.cpp",
]
SPDX-FileCopyrightText = "2026 najaeda contributors"
Expand Down
3 changes: 3 additions & 0 deletions docs/source_information.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ What fills them in najaeda 0.7.29:
named nets and instantiations; origins for lowered logic. Provider
`slang`, language `systemverilog`, representation `rtl`. The legacy slot
is still written too, and the viewer hides it when typed references exist.
- **VHDL** (since najaeda 0.7.32): declarations for entities, ports,
signals and instantiations (provider `naja-vhdl`, language `vhdl`,
representation `rtl`).
- **Gate-level Verilog**: every design, instance, term and declared net
gets its netlist declaration (provider `naja-verilog`, language
`verilog`). The spans are partial: a module's keyword and name, an
Expand Down
18 changes: 10 additions & 8 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from najaeda import netlist
import naja_schematic

netlist.load_primitives("xilinx")
top = netlist.load_verilog("design.v")
top = netlist.load_gate_level_verilog("design.v")

naja_schematic.show() # interactive view in the cell output
```
Expand All @@ -43,23 +43,25 @@ view
```

The view and your najaeda code can point each other at instances.
`show_instance()` takes a najaeda `Instance`, a list of instance ids (as
`Instance.pathIDs`) or a list of instance names (a plain string is one
`show_instance()` takes a najaeda `Instance` or a naja `SNLPath`, a list
of instance ids (as `Instance.pathIDs`) or a list of instance names (a plain string is one
name, never split on `/`, since escaped names can contain it). Anonymous
instances have no name to go by: reach them with an `Instance` or ids. The viewer opens its tree down to that instance, selects it, and
draws it alone in the schematic. Each pin there is open, so you can click
one to add its net and grow the schematic from the instance. In the other
direction, the instance selected in the viewer (click it in the tree or the
schematic) is available as a najaeda `Instance`:
schematic) is available as a naja `SNLPath` (najaeda's raw API), and its
ids, which give the najaeda `Instance`, as `selected_id_path`:

```python
view.show_instance(netlist.get_instance_by_path(["u_sub", "u_and"]))
view.show_instance(["u_sub", "u_and"]) # same thing

inst = view.selected # what the user clicked, or None
inst.get_model_name()
path = view.selected # what the user clicked (an SNLPath), or None
path.getModel().getName()
inst = netlist.Instance(view.selected_id_path)

view.on_select(lambda inst: print("selected", inst))
view.on_select(lambda path: print("selected", path))
```

`naja_schematic.show(inst)` (or `show(["u_sub", "u_and"])`) starts a new
Expand Down Expand Up @@ -95,7 +97,7 @@ server.annotate(items) # diagnosis overlay, kept across reloads
server.show_instance([3, 7]) # instance ids, names, or a najaeda Instance
server.on_select(lambda id_path, path: print("selected", path))
with server.replacing_design(): # holds design_lock, then viewers reload
netlist.load_verilog("other.v")
netlist.load_gate_level_verilog("other.v")
server.stop()
```

Expand Down
45 changes: 33 additions & 12 deletions python/naja_schematic/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
log = logging.getLogger("naja_schematic")

# 0.7.29: typed source references (getSourceDeclaration/getSourceOrigins).
MIN_NAJAEDA_VERSION = (0, 7, 29) # keep in step with pyproject.toml
# 0.7.31: load_gate_level_verilog/GateLevelVerilogConfig (load_verilog and
# VerilogConfig now warn).
# 0.7.34: word-level operator checks (isAddSub, isMultiply, isShift, isDivMod,
# ...) and loadSystemVerilog's frontend_options.
MIN_NAJAEDA_VERSION = (0, 7, 34) # keep in step with pyproject.toml


def check_najaeda_version():
Expand Down Expand Up @@ -64,10 +68,10 @@ def has_visible_primitive_instances(design):
# sequential model) -- never from its name. A cell naja has no model for is
# "unknown" and draws as the generic box. LocalSNLProvider.cpp's
# getPrimitiveType() mirrors this, same as the rest of the wire protocol.
# The truth-table checks are only asked of a cell with exactly one output
# bit: on a multi-output cell (NLDB0's full adder, a multi-output Liberty
# cell) naja's getTruthTable() throws, and najaeda doesn't translate that
# exception -- it aborts the process.
# The gate checks are only asked of a cell with exactly one output bit: a
# multi-output cell (NLDB0's full adder, a multi-output Liberty cell) is
# never drawn as a gate. A mux and the NLDB0 word-level operators are
# recognized at any width (a bus mux is one NLDB0 mux2 with bus pins).
_PRIMITIVE_TYPE_CHECKS = (
("isConst0", "tie0"),
("isConst1", "tie1"),
Expand All @@ -88,6 +92,18 @@ def get_primitive_type(model):
return "assign"
if model.isSequential():
return "dff"
if model.isAddSub():
return "sub" if model.isSubtract() else "add"
if model.isMultiply():
return "mul"
if model.isShift():
if not model.isShiftRight():
return "shl"
return "ashr" if model.isShiftArithmetic() else "shr"
if model.isDivMod():
return "divmod"
if model.isMux():
return "mux"
if output_bit_count(model) != 1:
return "unknown"
for check, ptype in _PRIMITIVE_TYPE_CHECKS:
Expand Down Expand Up @@ -1051,8 +1067,12 @@ def handle_message(message):
return [json.dumps(r) for r in handle_request(json.loads(message))]


def _instance_id_path(instance):
# A najaeda netlist.Instance's id path (top excluded); None for anything else.
def instance_id_path(instance):
# The id path (top excluded) of a naja SNLPath, or of a najaeda
# netlist.Instance (read through its pathIDs, without using netlist);
# None for anything else.
if isinstance(instance, naja.SNLPath):
return list(instance.getInstanceIDs())
ids = getattr(instance, "pathIDs", None)
return list(ids) if ids is not None else None

Expand All @@ -1072,9 +1092,10 @@ def _instance_name_path(ids):


def _diagnosis_item(item):
# An item whose "path" is a najaeda netlist.Instance gets its id_path
# (what the viewer matches first) and the matching name path.
ids = _instance_id_path(item.get("path"))
# An item whose "path" is a naja SNLPath (or a najaeda netlist.Instance)
# gets its id_path (what the viewer matches first) and the matching name
# path.
ids = instance_id_path(item.get("path"))
if ids is None:
return item
return {**item, "path": _instance_name_path(ids), "id_path": ids}
Expand All @@ -1085,8 +1106,8 @@ def diagnosis_response(items):
items (dicts with kind/path/id_path/terminal/severity/message/source),
or from a {"items": [...]} document as File > Load Diagnosis JSON...
accepts. An item names its instance by "id_path" (instance ids,
preferred) and/or "path" (instance names); "path" may also be a najaeda
netlist.Instance, turned into both here."""
preferred) and/or "path" (instance names); "path" may also be a naja
SNLPath or a najaeda netlist.Instance, turned into both here."""
if isinstance(items, dict):
items = items.get("items", [])
return {"response": "diagnosis_response", "items": [_diagnosis_item(i) for i in items]}
Expand Down
Loading
Loading