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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
CMAKE_BUILD_PARALLEL_LEVEL: "4"
run: |
python -m pip install --upgrade pip
python -m pip install --no-binary=najaeda "najaeda>=0.7.27"
python -m pip install --no-binary=najaeda "najaeda>=0.7.29"

- name: Install package + test dependencies from wheels
if: ${{ !matrix.najaeda_source_build }}
Expand Down
30 changes: 17 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,29 @@ CVA6 regressions, check the latest upstream CVA6 release and default-branch
revision to see whether the pinned baseline can be updated. Do not leave
CVA6 pinned indefinitely without retrying newer revisions.

As of 2026-10-06, the verified local regression baseline is CVA6 v5.3.0
As of 2026-10-13, the verified local regression baseline is CVA6 v5.3.0
(`2ef1c1b1fca419354920c5487293bc605294904e`), configuration
`cv32a6_imac_sv32`, with najaeda 0.7.27. All three tests in
`cv32a6_imac_sv32`, with najaeda 0.7.29. All three tests in
`tests/test_zzz_cone_cva6.py` and `tests/test_zzz_hierarchy_cva6.py` pass
against its rebuilt snapshot. The demo also pins v5.3.0 in
`examples/_cva6_fetch.sh`; its default `cv64a6_imafdc_sv39` demo also passes
with najaeda 0.7.27.
against its rebuilt snapshot (built from an isolated v5.3.0 checkout with
pinned submodules). The demo also pins v5.3.0 in `examples/_cva6_fetch.sh`;
its default `cv64a6_imafdc_sv39` configuration also elaborates with najaeda
0.7.29.

The latest upstream release remains v5.3.0. Upstream master
`81245a47fad8fe1a5d562d953ef2662e099def76` was retried on 2026-10-06 in an
isolated checkout with its pinned submodules and najaeda 0.7.27. The
`cv32a6_imac_sv32` raw-API elaboration still fails: the MMU's `always_comb`
`81245a47fad8fe1a5d562d953ef2662e099def76` (unchanged since 2026-10-06) was
retried on 2026-10-13 in an isolated checkout with its pinned submodules and
najaeda 0.7.29. The `cv32a6_imac_sv32` raw-API elaboration still fails, but
with 3 unsupported elements instead of 7: the MMU's `always_comb`
condition contains an `InvalidExpression` at `core/cva6_mmu/cva6_mmu.sv:404`,
and six continuous assignments in hpdcache report unsupported RHS calls
(lines 1196, 1204, 1212, 1268, 1269, 1270). The related upstream issue
and two continuous assignments in hpdcache report unsupported RHS calls
(lines 1212 and 1270; lines 1196, 1204, 1268 and 1269 now pass). The related
upstream issue
[CVA6 #3594](https://github.com/openhwfoundation/cva6/issues/3594), covering
the reversed Sv32 part-select, remains open with no comments. Retain the
working baseline, and check the issue status and retry newer revisions on
the next upgrade rather than assuming these limitations remain.
the reversed Sv32 part-select, remains open (one comment, a contributor
volunteering a fix). Retain the working baseline, and check the issue status
and retry newer revisions on the next upgrade rather than assuming these
limitations remain.

Test candidate revisions in an isolated checkout with their pinned
submodules, preserving the user's working checkout. Elaborate through the
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,20 +169,22 @@ way as RTL:
> `data_out`."*

Hierarchy, per-cell counts (`get_module_card`), drivers/loads, and logic cones
all work on the netlist; cones stop at the sequential cells. A gate netlist
carries no source line info, so `get_source` applies to RTL only. A runnable
all work on the netlist; cones stop at the sequential cells. `get_source`
shows a cell's or net's line in the netlist. For Yosys output, load with
`yosys_source_attributes=True`: the `(* src = ... *)` attributes become
`origins` that `get_source(path, origin=0)` reads back in the RTL. A runnable
example lives in [`examples/`](examples/) (`stdcells.lib` + `counter2.v` +
`gate_level.py`).

---

## VHDL (beta)

VHDL loading is available in beta with najaeda 0.7.25 or newer. Call
`load_vhdl(file="/path/to/design.vhd", top="my_entity")` to explore its
elaborated hierarchy and connectivity. Load dependencies/packages first,
one file per call; package-only files may return `top: null` until the top
file is loaded. The frontend supports a restricted two-state RTL subset,
VHDL loading is available in beta. Call
`load_vhdl(file=["pkg.vhd", "child.vhd", "top.vhd"], top="my_entity")` to
explore its elaborated hierarchy and connectivity; the files may be listed in
any order. A single path per call also works (load packages first);
package-only files may return `top: null` until the top file is loaded. The frontend supports a restricted two-state RTL subset,
and supported constructs may change. `get_intent`/`load_intent` remain
SystemVerilog-only; VHDL source ranges are not guaranteed.

Expand Down Expand Up @@ -275,7 +277,7 @@ Apache-2.0. See [LICENSE](LICENSE).

## Optional browser schematic

Requires **naja-schematic 0.1.6 or later** (the release containing the
Requires **naja-schematic 0.2.0 or later** (the release containing the
embeddable viewer API):

```bash
Expand Down
17 changes: 9 additions & 8 deletions README_PyPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,19 @@ way as RTL:
> `data_out`."*

Hierarchy, per-cell counts, drivers/loads, and logic cones all work on the
netlist; cones stop at the sequential cells. (A gate netlist carries no source
line info, so `get_source` applies to RTL only.)
netlist; cones stop at the sequential cells. `get_source` shows an object's
line in the netlist, and for Yosys output `yosys_source_attributes=True` adds
`origins` that `get_source(path, origin=0)` reads back in the RTL.

---

## VHDL (beta)

VHDL loading is available in beta with najaeda 0.7.25 or newer. Call
`load_vhdl(file="/path/to/design.vhd", top="my_entity")` to explore its
elaborated hierarchy and connectivity. Load dependencies/packages first,
one file per call; package-only files may return `top: null` until the top
file is loaded. The frontend supports a restricted two-state RTL subset,
VHDL loading is available in beta. Call
`load_vhdl(file=["pkg.vhd", "child.vhd", "top.vhd"], top="my_entity")` to
explore its elaborated hierarchy and connectivity; the files may be listed in
any order. A single path per call also works (load packages first);
package-only files may return `top: null` until the top file is loaded. The frontend supports a restricted two-state RTL subset,
and supported constructs may change. `get_intent`/`load_intent` remain
SystemVerilog-only; VHDL source ranges are not guaranteed.

Expand Down Expand Up @@ -211,7 +212,7 @@ Apache-2.0. See [LICENSE](https://github.com/keplertech/naja-scope/blob/main/LIC

## Optional browser schematic

Requires **naja-schematic 0.1.6 or later** (the release containing the
Requires **naja-schematic 0.2.0 or later** (the release containing the
embeddable viewer API):

```bash
Expand Down
5 changes: 3 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ It shows:
stage entirely, into `csr_regfile_i` (the privilege-level register gates
the divider) — a fact no textual grep of `serdiv.sv` could ever reveal.
- **A whole-port fan-in cone** — `trace_cone` on the top-level output
`noc_req_o` (470 bits) traces all 2.8M nodes of combinational logic behind
it in one call, ~2.5 minutes.
`noc_req_o` (470 bits) shows that the whole request is registered inside
the cache subsystem's AXI adapter: the cone stops at the adapter's FIFO
storage and state registers, without reaching back into the caches.

This is the same tour that runs in CI as a regression
(`.github/workflows/cva6-demo.yml`) — MCP-only, no agent, so it's
Expand Down
12 changes: 7 additions & 5 deletions examples/core_v_mcu_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ def main():
print(f" flattened leaf gates : {root['flat_leaves']:,}")
print(f" sequential instances : {root['flat_sequential']:,}")
print(f" distinct model variants : {stats['total_models']}")
# Revalidated on the pinned CORE-V-MCU commit with najaeda 0.7.27.
# Its wildcard-case sequential lowering changes the CSR implementation:
# 0.7.25 produced 722,402 leaves / 97,376 sequential instances. These
# count native instances, including word-wide cells, rather than RTL bits.
assert root["flat_leaves"] == 634_875, "elaborated leaf count drifted"
# Revalidated on the pinned CORE-V-MCU commit with najaeda 0.7.29, whose
# combinational lowering yields 631,603 leaves (634,875 with 0.7.27;
# sequential and model counts unchanged). 0.7.27's wildcard-case
# sequential lowering had changed the CSR implementation: 0.7.25 produced
# 722,402 leaves / 97,376 sequential instances. These count native
# instances, including word-wide cells, rather than RTL bits.
assert root["flat_leaves"] == 631_603, "elaborated leaf count drifted"
assert root["flat_sequential"] == 6_501, "sequential instance count drifted"
assert stats["total_models"] == 320, "elaborated model count drifted"

Expand Down
11 changes: 9 additions & 2 deletions examples/cva6_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,16 @@ def main():
print(f" nodes in cone : {cone['node_count']}")
print(f" register frontier : {frontier['flop_count']} flop(s), "
f"{frontier['blackbox_count']} black box(es)")
assert cone["node_count"] > 100_000, (
"expected a large cone spanning most of the core's combinational logic")
homes = sorted({".".join(f["path"].split(".")[:3]) for f in frontier["flops"]})
print(f" frontier lives in : {homes}")
# The request is registered inside the AXI adapter: its I/D-cache data
# FIFOs are not fall-through, so the true fan-in stops at the adapter's
# FIFO storage and state registers. najaeda <=0.7.27 over-approximated
# this, running through the FIFOs into the caches (~2.5M nodes).
assert frontier["flop_count"] > 0, "expected a non-trivial register frontier"
assert frontier["blackbox_count"] == 0, "cone should fully traverse combinational logic"
assert all("i_adapter" in f["path"] for f in frontier["flops"]), (
"expected noc_req_o to be registered inside the AXI adapter")

print("\nDone. A handful of small, exact calls answered structural, "
"connectivity and cross-hierarchy questions on a real RISC-V core "
Expand Down
5 changes: 3 additions & 2 deletions examples/gate_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
tests/test_examples_gate_level.py asserts these same answers, so this tour
can't silently drift from how the tools actually behave.

Note: a gate netlist carries no SystemVerilog source info, so get_source has
nothing to point at here — gate-level is about structure and connectivity.
Note: get_source on a gate netlist points at the object's line in the .v
file; for Yosys output, load_verilog(..., yosys_source_attributes=True) also
exposes the RTL lines behind each cell as `origins`.
"""

import os
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ classifiers = [
"Topic :: Software Development :: Libraries",
]
dependencies = [
"najaeda>=0.7.27",
"najaeda>=0.7.29",
"mcp>=1.28,<2",
]

[project.optional-dependencies]
schematic = ["naja-schematic>=0.1.6,<0.2"]
schematic = ["naja-schematic>=0.2.0,<0.3"]

[project.urls]
Homepage = "https://github.com/keplertech/naja-scope"
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"url": "https://github.com/keplertech/naja-scope",
"source": "github"
},
"version": "0.1.18",
"version": "0.1.19",
"packages": [
{
"registryType": "pypi",
"identifier": "naja-scope",
"version": "0.1.18",
"version": "0.1.19",
"transport": {
"type": "stdio"
}
Expand Down
2 changes: 1 addition & 1 deletion src/naja_scope/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
"""naja-scope: agent-facing MCP query layer over najaeda."""

__version__ = "0.1.18"
__version__ = "0.1.19"
44 changes: 30 additions & 14 deletions src/naja_scope/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import fnmatch
import io
import os
from typing import List, Optional
from typing import List, Optional, Union

from najaeda import naja

Expand All @@ -19,8 +19,8 @@
from . import snl
from .errors import ScopeError
from .paging import clamp_limit, paginate
from .resolve import (Resolved, describe, resolve_path, source_range,
source_ref)
from .resolve import (MAX_ORIGINS, Resolved, describe, resolve_path,
source_origins, source_range, source_ref)
from .session import SESSION
from .runtime import serialized, design_mutation

Expand Down Expand Up @@ -102,7 +102,8 @@ def load_systemverilog(files: Optional[List[str]] = None,
keep_assigns: bool = True,
intent: bool = False,
defines: Optional[List[str]] = None,
allow_unknown_designs: bool = False) -> dict:
allow_unknown_designs: bool = False,
blackbox_multi_writer_memories: bool = False) -> dict:
# Warm load: retain the slang AST link inline (keep_ast_link) when intent is
# requested — no separate elaboration step. Off by default (the Compilation
# is GB-class on large designs).
Expand All @@ -112,7 +113,8 @@ def load_systemverilog(files: Optional[List[str]] = None,
keep_assigns=keep_assigns,
keep_ast_link=want,
defines=defines,
allow_unknown_designs=allow_unknown_designs)
allow_unknown_designs=allow_unknown_designs,
blackbox_multi_writer_memories=blackbox_multi_writer_memories)
out = {"top": _summary(top_instance)}
if want:
out["intent_loaded"] = SESSION.intent_available
Expand All @@ -136,16 +138,18 @@ def load_intent(flist: Optional[str] = None, files: Optional[List[str]] = None,

@serialized
def load_verilog(files: List[str], keep_assigns: bool = True,
allow_unknown_designs: bool = False) -> dict:
allow_unknown_designs: bool = False,
yosys_source_attributes: bool = False) -> dict:
top_instance = SESSION.load_verilog(
files, keep_assigns=keep_assigns,
allow_unknown_designs=allow_unknown_designs)
allow_unknown_designs=allow_unknown_designs,
yosys_source_attributes=yosys_source_attributes)
return {"top": _summary(top_instance)}


@serialized
def load_vhdl(file: str, top: Optional[str] = None) -> dict:
"""Load a single VHDL file using najaeda's beta frontend."""
def load_vhdl(file: Union[str, List[str]], top: Optional[str] = None) -> dict:
"""Load one VHDL file, or a batch in any order, with najaeda's beta frontend."""
node = SESSION.load_vhdl(file, top=top)
out = {"top": _summary(node) if node is not None else None,
"language": "vhdl", "beta": True, "intent_loaded": False}
Expand Down Expand Up @@ -349,21 +353,32 @@ def trace_cone(path: str, direction: str,
# -- source -----------------------------------------------------------------------

@serialized
def get_source(path: str, context_lines: int = DEFAULT_SOURCE_CONTEXT) -> dict:
def get_source(path: str, context_lines: int = DEFAULT_SOURCE_CONTEXT,
origin: Optional[int] = None) -> dict:
context_lines = max(0, min(context_lines, 20))
matches = resolve_path(SESSION, path)
resolved = matches[0]
rng = source_range(resolved)
origins = source_origins(resolved)[:MAX_ORIGINS]
extra = {"origins": [r.to_ref() for r in origins]} if origins else {}
if origin is not None:
if not 0 <= origin < len(origins):
return {"object": resolved.path, **extra,
"error": f"No source origin #{origin} for this object "
f"({len(origins)} known)."}
rng = origins[origin]
extra["origin"] = origin
if rng is None:
return {
"object": resolved.path,
"error": "No source range known for this object.",
"hint": ("Source ranges come from SystemVerilog loading; "
"gate-level Verilog without source info has none."),
"hint": ("Source ranges come from SystemVerilog loading and "
"Verilog netlists; VHDL beta objects and synthesized "
"anonymous objects may have none."),
}
file_path = SESSION.find_source_file(rng.file)
if file_path is None:
return {"object": resolved.path, "src": rng.to_ref(),
return {"object": resolved.path, "src": rng.to_ref(), **extra,
"error": f"Source file not found: {rng.file}"}
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
Expand All @@ -375,7 +390,7 @@ def get_source(path: str, context_lines: int = DEFAULT_SOURCE_CONTEXT) -> dict:
# etc.) are visible in one call. Stops at a blank line, and does nothing when
# the type is referenced (e.g. a package typedef `riscv::priv_lvl_t x;`) with
# no block above — exactly the case that needs the intent layer.
if resolved.kind in ("net", "term"):
if resolved.kind in ("net", "term") and origin is None:
lo = max(1, rng.line - _DECL_MAX_UP)
probe = rng.line
while probe > lo:
Expand All @@ -401,6 +416,7 @@ def get_source(path: str, context_lines: int = DEFAULT_SOURCE_CONTEXT) -> dict:
"start": start,
"end": end,
"src": rng.to_ref(),
**extra,
"text": "".join(selected),
"truncated": truncated,
}
Expand Down
12 changes: 6 additions & 6 deletions src/naja_scope/cone.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
only. naja-scope no longer hand-rolls the equipotential BFS; it seeds the cone,
then turns the DAG into a token-bounded summary:

* `get_node_count()` / `get_nodes()` kinds -> size + counts_by_kind,
* `get_leaves()` -> the stop-at-flops frontier
* `getNodeCount()` / `getNodes()` kinds -> size + counts_by_kind,
* `getLeaves()` -> the stop-at-flops frontier
(flops, top ports, opaque black boxes), categorised and bounded,
* the flop frontier grouped by top-level submodule, naming the registers that
lie OUTSIDE the cone root's own subtree -> the cross-hierarchy answer.
Expand Down Expand Up @@ -40,7 +40,7 @@
"fanin": naja.LogicCone.FanIn,
"fanout": naja.LogicCone.FanOut,
}
# get_leaves() kinds that are barriers, by category.
# getLeaves() kinds that are barriers, by category.
_LEAF_KINDS = {"flop", "ports", "blackbox"}


Expand Down Expand Up @@ -149,10 +149,10 @@ def trace_cone(resolved: Resolved, session, direction: str,
if occ is None:
continue
cone = naja.LogicCone(occ, cone_dir)
node_count += cone.get_node_count()
for (_id, _occ, kind, _nx, _pv) in cone.get_nodes():
node_count += cone.getNodeCount()
for (_id, _occ, kind, _nx, _pv) in cone.getNodes():
counts_by_kind[kind] = counts_by_kind.get(kind, 0) + 1
for (_id, leaf_occ, kind, _nx, _pv) in cone.get_leaves():
for (_id, leaf_occ, kind, _nx, _pv) in cone.getLeaves():
if kind == "ports":
nc = leaf_occ.getNetComponent()
if nc is None:
Expand Down
Loading
Loading