From 761f2f0ac92420e1375c193c6cc1b7fb3eb0ba98 Mon Sep 17 00:00:00 2001 From: xtof Date: Tue, 13 Oct 2026 08:48:21 +0200 Subject: [PATCH 1/3] Prepare naja-scope 0.1.19 with najaeda 0.7.29 Upgrade to najaeda 0.7.29 and adopt its new raw-API features: - Follow the camelCase-only raw binding: LogicCone getNodeCount/getNodes/ getLeaves and naja.intentAvailable/intentTypeOf/intentParametersOf/ intentPackageMember (snake_case aliases were removed upstream). - Source references: read the language-neutral getSourceDeclaration / getSourceOrigins, falling back to getSourceLoc. Gate-level Verilog netlists now answer get_source, and extra origins are listed as `origins`. - load_verilog(yosys_source_attributes=True) decodes Yosys src attributes into RTL origins; get_source(path, origin=N) reads them. - load_vhdl accepts a list of files loaded as one batch in any order. - load_systemverilog(blackbox_multi_writer_memories=True) blackboxes modules with unsupported multi-writer memories instead of failing. - Revalidate the CORE-V-MCU demo baseline (631,603 leaves) and record the CVA6 v5.3.0 / master recheck in CLAUDE.md. --- .github/workflows/ci.yml | 2 +- CLAUDE.md | 30 +++++++------ README.md | 16 ++++--- README_PyPI.md | 15 ++++--- examples/core_v_mcu_demo.py | 12 +++--- examples/gate_level.py | 5 ++- pyproject.toml | 2 +- server.json | 4 +- src/naja_scope/__init__.py | 2 +- src/naja_scope/api.py | 44 +++++++++++++------ src/naja_scope/cone.py | 12 +++--- src/naja_scope/intent.py | 20 ++++----- src/naja_scope/loader.py | 41 ++++++++++++------ src/naja_scope/resolve.py | 21 ++++++++- src/naja_scope/server.py | 59 +++++++++++++++++-------- src/naja_scope/session.py | 30 ++++++++----- src/naja_scope/snl.py | 69 ++++++++++++++++++++++++----- tests/test_intent.py | 4 +- tests/test_net_types.py | 2 +- tests/test_netlist_source.py | 81 +++++++++++++++++++++++++++++++++++ tests/test_sv_load_options.py | 41 ++++++++++++++++++ tests/test_vhdl_load.py | 33 ++++++++++++++ 22 files changed, 418 insertions(+), 127 deletions(-) create mode 100644 tests/test_netlist_source.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c93f558..93d1cce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} diff --git a/CLAUDE.md b/CLAUDE.md index 7aa9e69..54c6403 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index 6a33ae4..b3767ec 100644 --- a/README.md +++ b/README.md @@ -169,8 +169,10 @@ 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`). @@ -178,11 +180,11 @@ example lives in [`examples/`](examples/) (`stdcells.lib` + `counter2.v` + ## 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. diff --git a/README_PyPI.md b/README_PyPI.md index f3f50b6..2c61c4c 100644 --- a/README_PyPI.md +++ b/README_PyPI.md @@ -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. diff --git a/examples/core_v_mcu_demo.py b/examples/core_v_mcu_demo.py index 4154726..efc65ec 100755 --- a/examples/core_v_mcu_demo.py +++ b/examples/core_v_mcu_demo.py @@ -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" diff --git a/examples/gate_level.py b/examples/gate_level.py index 8014484..144c062 100644 --- a/examples/gate_level.py +++ b/examples/gate_level.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index d78680f..c47004e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "najaeda>=0.7.27", + "najaeda>=0.7.29", "mcp>=1.28,<2", ] diff --git a/server.json b/server.json index 2d6bb1c..45d317a 100644 --- a/server.json +++ b/server.json @@ -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" } diff --git a/src/naja_scope/__init__.py b/src/naja_scope/__init__.py index aeb2a1e..64e5952 100644 --- a/src/naja_scope/__init__.py +++ b/src/naja_scope/__init__.py @@ -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" diff --git a/src/naja_scope/api.py b/src/naja_scope/api.py index 0a21423..c7cb7ce 100644 --- a/src/naja_scope/api.py +++ b/src/naja_scope/api.py @@ -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 @@ -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 @@ -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). @@ -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 @@ -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} @@ -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() @@ -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: @@ -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, } diff --git a/src/naja_scope/cone.py b/src/naja_scope/cone.py index b5d64ea..61e71e6 100644 --- a/src/naja_scope/cone.py +++ b/src/naja_scope/cone.py @@ -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. @@ -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"} @@ -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: diff --git a/src/naja_scope/intent.py b/src/naja_scope/intent.py index 16cf438..aa37a5e 100644 --- a/src/naja_scope/intent.py +++ b/src/naja_scope/intent.py @@ -3,15 +3,15 @@ Recovers the source-level facts erased by SystemVerilog lowering — enum/FSM state names + encodings, declared type names, and symbolic parameter expressions — by -calling naja's curated intent API (`naja.intent_type_of` / `intent_parameters_of` -/ `intent_package_member`). Those walk the live slang AST *in C++*, over the +calling naja's curated intent API (`naja.intentTypeOf` / `intentParametersOf` +/ `intentPackageMember`). Those walk the live slang AST *in C++*, over the SNL↔slang bimap naja built at lowering time (`keep_ast_link=True`), and return plain Python data. There is NO pyslang here and no second elaboration: naja owns the one AST and answers in dicts. Warm-only (the slang Compilation never serializes): the link exists only while it is alive in-process — i.e. after a SystemVerilog load with -`keep_ast_link`. `naja.intent_available()` reports it. A cold snapshot load has no +`keep_ast_link`. `naja.intentAvailable()` reports it. A cold snapshot load has no Compilation, so intent degrades to "not loaded" until a warm (re)load; the exact relink-after-snapshot tier is deferred. @@ -19,7 +19,7 @@ object (`resolve.py`), then handed to `naja.intent_*`. This is why anonymous lowered primitives (`#`) now answer — they resolve to the FF instance, which the bimap maps back to its declaring source symbol. Package members (`pkg::name`) -have no SNL object and go through the name-keyed `naja.intent_package_member`. +have no SNL object and go through the name-keyed `naja.intentPackageMember`. """ from __future__ import annotations @@ -60,7 +60,7 @@ class IntentProvider: structural ref→object resolution. Stateless beyond the session handle: the live AST is owned by naja (kept - alive by `keep_ast_link`), so "loaded" is simply `naja.intent_available()`. + alive by `keep_ast_link`), so "loaded" is simply `naja.intentAvailable()`. Plugs into Session as the IntentProvider seam. """ @@ -69,7 +69,7 @@ def __init__(self, session): @property def loaded(self) -> bool: - return bool(naja.intent_available()) + return bool(naja.intentAvailable()) # -- public queries ------------------------------------------------------ @@ -78,7 +78,7 @@ def get_type(self, ref: str) -> dict: if "::" in ref: return self._package_record(ref) resolved = resolve_path(self.session, ref)[0] - rec = naja.intent_type_of(self._intent_obj(resolved)) + rec = naja.intentTypeOf(self._intent_obj(resolved)) if rec is None: return {"ref": ref, "label": resolved.path.split(".")[-1], "note": (f"no source type recovered for {ref} (non-enum " @@ -98,7 +98,7 @@ def get_fsm_states(self, ref: str) -> dict: def get_parameters(self, ref: str, symbolic: bool = True) -> dict: """Parameters + localparams of an instance/module, with their SYMBOLIC initializer text (lost in lowering) alongside the elaborated value.""" - rec = naja.intent_parameters_of(self._resolve_instance_obj(ref)) + rec = naja.intentParametersOf(self._resolve_instance_obj(ref)) if rec is None: raise ScopeError( f"{ref!r}: no parameters recovered (not an instance/module with " @@ -121,7 +121,7 @@ def describe(self, ref: str) -> dict: # -- resolution helpers -------------------------------------------------- def _intent_obj(self, resolved): - """The SNL object to hand to naja.intent_type_of for a resolved ref: + """The SNL object to hand to naja.intentTypeOf for a resolved ref: the raw net/term, or — for an instance — its SNLInstance (top: design).""" if resolved.kind == "instance": node = resolved.obj @@ -145,7 +145,7 @@ def _package_record(self, ref: str) -> dict: """A package member (pkg::name) — name-keyed over the live compilation, since packages are not instantiated and have no SNL object.""" pkg, _, member = ref.partition("::") - rec = naja.intent_package_member(pkg, member) + rec = naja.intentPackageMember(pkg, member) if rec is None: raise ScopeError( f"intent: {member!r} not found in package {pkg!r} (or the intent " diff --git a/src/naja_scope/loader.py b/src/naja_scope/loader.py index 90fbf2e..bedb4d6 100644 --- a/src/naja_scope/loader.py +++ b/src/naja_scope/loader.py @@ -17,14 +17,14 @@ import os import re import tempfile -from typing import List, Optional +from typing import List, Optional, Union from najaeda import naja from .errors import ScopeError, SVInternalError, SVSyntaxError, SVUnsupportedError # najaeda>=0.7.9 introduced these typed exceptions; naja-scope currently -# requires najaeda>=0.7.27. +# requires najaeda>=0.7.29. # directly from loadSystemVerilog; anything loaded out-of-band below that floor # (e.g. via NAJAEDA_SRC pointing at an older checkout) only raises plain # RuntimeError. Detect once so classification degrades gracefully instead of @@ -82,7 +82,8 @@ def load_systemverilog(files: List[str], flist: Optional[str] = None, top: Optional[str] = None, keep_assigns: bool = True, keep_ast_link: bool = False, defines: Optional[List[str]] = None, - allow_unknown_designs: bool = False): + allow_unknown_designs: bool = False, + blackbox_multi_writer_memories: bool = False): db = get_top_db() effective_flist = flist temp_flist = None @@ -108,7 +109,7 @@ def load_systemverilog(files: List[str], flist: Optional[str] = None, suppress_warnings=None, defines=defines, # Retain the live slang Compilation + SNL<->slang bimap for the - # intent layer (naja.intent_*). Off by default (the Compilation is + # intent layer (naja.intent*). Off by default (the Compilation is # GB-class on large designs); enabled only when the intent layer is # requested. keep_ast_link=keep_ast_link, @@ -116,6 +117,9 @@ def load_systemverilog(files: List[str], flist: Optional[str] = None, # kwarg on both loadVerilog and loadSystemVerilog is # blackbox_unknown_modules (see the loadVerilog comment above). blackbox_unknown_modules=allow_unknown_designs, + # najaeda>=0.7.29: blackbox a whole module whose memory has + # multiple writers (unsupported) instead of failing elaboration. + blackbox_multi_writer_memories=blackbox_multi_writer_memories, ) except RuntimeError as e: raise _classify_sv_load_error(e) from e @@ -125,7 +129,8 @@ def load_systemverilog(files: List[str], flist: Optional[str] = None, def load_verilog(files: List[str], keep_assigns: bool = True, - allow_unknown_designs: bool = False): + allow_unknown_designs: bool = False, + yosys_source_attributes: bool = False): # najaeda>=0.7.11 renamed the raw NLDB.loadVerilog kwarg from # allow_unknown_designs to blackbox_unknown_modules; naja-scope's own # public parameter name is kept stable across that rename. @@ -135,24 +140,32 @@ def load_verilog(files: List[str], keep_assigns: bool = True, blackbox_unknown_modules=allow_unknown_designs, preprocess_enabled=False, conflicting_design_name_policy="forbid", + # najaeda>=0.7.29: decode Yosys `(* src = ... *)` attributes into + # source origins, cross-probing netlist objects back to the RTL. + yosys_source_attributes=yosys_source_attributes, ) -def load_vhdl(file: str, top: Optional[str] = None): - """Load one VHDL source through the raw beta frontend. +def load_vhdl(file: Union[str, List[str]], top: Optional[str] = None): + """Load one VHDL source, or a batch, through the raw beta frontend. - Package-only files and entities awaiting generic values may return None. - Disable the default diagnostics file; diagnostics still go to stderr. + A batch (najaeda>=0.7.29) may list files in any order: units are resolved, + not merely sorted. Package-only files and entities awaiting generic values + may return None. Disable the default diagnostics file; diagnostics still go + to stderr. """ - if not file or not os.path.isfile(file): - raise ScopeError(f"VHDL source file not found: {file}") + files = [file] if isinstance(file, str) else list(file or []) + if not files: + raise ScopeError("No VHDL source files provided.") + for path in files: + if not path or not os.path.isfile(path): + raise ScopeError(f"VHDL source file not found: {path}") if top == "": raise ScopeError("VHDL top must not be empty; omit it to infer the top.") db = get_top_db() - if not hasattr(db, "loadVHDL"): - raise ScopeError("VHDL loading requires najaeda>=0.7.25.") try: - return db.loadVHDL(file, top=top, diagnostics_report_path=None) + return db.loadVHDL(files if len(files) > 1 else files[0], top=top, + diagnostics_report_path=None) except RuntimeError as exc: raise ScopeError(f"VHDL beta loading failed: {exc}") from exc diff --git a/src/naja_scope/resolve.py b/src/naja_scope/resolve.py index aa816f5..91a1b2c 100644 --- a/src/naja_scope/resolve.py +++ b/src/naja_scope/resolve.py @@ -85,11 +85,14 @@ def describe(resolved: Resolved, session=None) -> dict: src = source_ref(resolved, session) if src: out["src"] = src + origins = [r.to_ref() for r in source_origins(resolved)[:MAX_ORIGINS]] + if origins: + out["origins"] = origins return out def source_range(resolved: Resolved) -> Optional[SrcRange]: - """SrcRange for a resolved object, straight from getSourceLoc().""" + """SrcRange for a resolved object, straight from its SNL source refs.""" if resolved.kind == "instance": loc = resolved.obj.source_loc() else: @@ -100,11 +103,25 @@ def source_range(resolved: Resolved) -> Optional[SrcRange]: def source_ref(resolved: Resolved, session=None) -> Optional[str]: - """`file:start-end` for a resolved object, straight from getSourceLoc().""" + """`file:start-end` for a resolved object, straight from its SNL source refs.""" rng = source_range(resolved) return rng.to_ref() if rng else None +# Cap on upstream origins listed per object (a Yosys src attribute can merge +# many RTL locations into one cell). +MAX_ORIGINS = 8 + + +def source_origins(resolved: Resolved) -> List[SrcRange]: + """Upstream source origins (e.g. Yosys src -> RTL) beyond source_range.""" + if resolved.kind == "instance": + locs = resolved.obj.source_origins() + else: + locs = snl.source_origins(resolved.obj) + return [SrcRange.from_loc(loc) for loc in locs] + + def _level_names(node: snl.InstNode, cap: int = _MAX_SUGGESTION_SCAN): """(child names, term names, net names) visible at one hierarchy level.""" children, terms, nets = [], [], [] diff --git a/src/naja_scope/server.py b/src/naja_scope/server.py index a46a275..5494983 100644 --- a/src/naja_scope/server.py +++ b/src/naja_scope/server.py @@ -10,7 +10,7 @@ import functools import os import sys -from typing import Annotated, Any, Callable, Dict, List, Literal, Optional +from typing import Annotated, Any, Callable, Dict, List, Literal, Optional, Union # Allow using a local najaeda checkout without installing it. _najaeda_src = os.getenv("NAJAEDA_SRC") @@ -153,6 +153,11 @@ def load_systemverilog( Field(description="Black-box unresolved module definitions instead " "of failing elaboration."), ] = False, + blackbox_multi_writer_memories: Annotated[ + bool, + Field(description="Black-box whole modules whose memories have " + "unsupported multiple writers instead of failing."), + ] = False, ) -> dict: """Elaborate local SystemVerilog sources into the active design session. Use this for SystemVerilog RTL; use load_vhdl for VHDL (beta), or @@ -162,10 +167,12 @@ def load_systemverilog( Anonymous lowered objects are addressable by #. defines are preprocessor -D entries ("NAME" or "NAME=VALUE"). allow_unknown_designs=True blackboxes any module still undefined instead of failing (e.g. undelivered - hard macros in a partly-open-source design). intent=True retains naja's - in-engine SNL↔slang link for get_intent.""" + hard macros in a partly-open-source design). blackbox_multi_writer_memories + does the same for modules whose multi-writer memories naja cannot infer. + intent=True retains naja's in-engine SNL↔slang link for get_intent.""" return api.load_systemverilog(files, flist, top, keep_assigns, intent, - defines, allow_unknown_designs) + defines, allow_unknown_designs, + blackbox_multi_writer_memories) @_tool(annotations=SESSION_MUTATION) @@ -182,20 +189,29 @@ def load_verilog( bool, Field(description="Black-box unresolved cell/module definitions instead of failing."), ] = False, + yosys_source_attributes: Annotated[ + bool, + Field(description="Decode Yosys `(* src = ... *)` attributes into " + "source origins pointing back to the RTL."), + ] = False, ) -> dict: """Load local gate-level or structural Verilog into the active session. First call load_liberty for Liberty cells or load_primitives for built-ins; use load_systemverilog instead for RTL elaboration. Unknown modules fail - unless `allow_unknown_designs` is true. Gate netlists carry no source info, - so get_source/get_intent cannot answer for them.""" - return api.load_verilog(files, keep_assigns, allow_unknown_designs) + unless `allow_unknown_designs` is true. get_source shows the netlist + declaration of an object; for Yosys output, yosys_source_attributes=True + also records its RTL `origins` (get_source origin=N). get_intent cannot + answer for netlists.""" + return api.load_verilog(files, keep_assigns, allow_unknown_designs, + yosys_source_attributes) @_tool(annotations=SESSION_MUTATION) def load_vhdl( file: Annotated[ - str, - Field(description="One local VHDL source file path (.vhd or .vhdl)."), + Union[str, List[str]], + Field(description="A local VHDL source path (.vhd/.vhdl), or a list " + "of paths loaded as one batch in any order."), ], top: Annotated[ Optional[str], @@ -203,8 +219,10 @@ def load_vhdl( ] = None, ) -> dict: """Load VHDL into the active session (beta, restricted two-state RTL subset). - Load dependency/package files first, one call per file, then the top file. - Package-only files or entities awaiting generics may return top=null. + Prefer passing every file of the design as one list (any order; units are + resolved across files). Single-file calls still work: load package files + first, then the top. Package-only files or entities awaiting generics may + return top=null. Use hierarchy, cards and connectivity queries on the elaborated design. SystemVerilog intent recovery is unavailable; source ranges are not guaranteed. Unsupported constructs may fail during beta development. @@ -447,20 +465,27 @@ def trace_cone( def get_source( path: Annotated[ str, - Field(description="Exact hierarchical path to an object from a SystemVerilog load."), + Field(description="Exact hierarchical path to a loaded design object."), ], context_lines: Annotated[ int, Field(description="Extra lines before and after the source range; clamped to 0..20."), ] = 3, + origin: Annotated[ + Optional[int], + Field(description="Index into the object's `origins` list to read an " + "upstream source (e.g. the RTL behind a Yosys " + "netlist cell) instead of its declaration."), + ] = None, ) -> dict: - """Read the bounded SystemVerilog source excerpt that produced an object - (for example, an FF instance maps to its `always_ff` block). Use after - load_systemverilog when exact source text is needed; use get_intent for - typedef/enum/parameter semantics and do not use for gate-level Verilog. + """Read the bounded source excerpt that produced an object (for example, + an SV FF instance maps to its `always_ff` block; a netlist cell maps to its + instantiation line). Use when exact source text is needed; use get_intent + for typedef/enum/parameter semantics. When the response lists `origins` + (e.g. Yosys src attributes), call again with origin=N to read that RTL. Returns object, file, start/end range, text, and truncation status without modifying files; missing paths and source ranges return structured errors.""" - return api.get_source(path, context_lines) + return api.get_source(path, context_lines, origin) @_tool(annotations=READ_ONLY) diff --git a/src/naja_scope/session.py b/src/naja_scope/session.py index 74ad08f..d6076b3 100644 --- a/src/naja_scope/session.py +++ b/src/naja_scope/session.py @@ -9,7 +9,7 @@ from __future__ import annotations import os -from typing import List, Optional +from typing import List, Optional, Union from najaeda import naja @@ -71,34 +71,39 @@ def load_systemverilog(self, files: List[str], flist: Optional[str] = None, keep_assigns: bool = True, keep_ast_link: bool = False, defines: Optional[List[str]] = None, - allow_unknown_designs: bool = False) -> "snl.InstNode": + allow_unknown_designs: bool = False, + blackbox_multi_writer_memories: bool = False) -> "snl.InstNode": loader.load_systemverilog(files, flist=flist, top=top, keep_assigns=keep_assigns, keep_ast_link=keep_ast_link, defines=defines, - allow_unknown_designs=allow_unknown_designs) + allow_unknown_designs=allow_unknown_designs, + blackbox_multi_writer_memories=blackbox_multi_writer_memories) self._record_sources(files) if flist: self._record_sources([flist]) self.load_spec = {"files": list(files or []), "flist": flist, "top": top, "defines": defines, - "allow_unknown_designs": allow_unknown_designs} + "allow_unknown_designs": allow_unknown_designs, + "blackbox_multi_writer_memories": blackbox_multi_writer_memories} return snl.top_node() @design_mutation def load_verilog(self, files: List[str], keep_assigns: bool = True, - allow_unknown_designs: bool = False) -> "snl.InstNode": + allow_unknown_designs: bool = False, + yosys_source_attributes: bool = False) -> "snl.InstNode": loader.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) self._record_sources(files) self.load_spec = {"files": list(files or []), "flist": None, "top": None} return snl.top_node() @design_mutation - def load_vhdl(self, file: str, top: Optional[str] = None): + def load_vhdl(self, file: Union[str, List[str]], top: Optional[str] = None): design = loader.load_vhdl(file, top=top) - self._record_sources([file]) + self._record_sources([file] if isinstance(file, str) else list(file)) self.load_spec = {"language": "vhdl", "files": list(self.loaded_files), "top": top} return snl.top_node() if design is not None else None @@ -122,7 +127,7 @@ def load_intent(self, flist: Optional[str] = None, if self.load_spec.get("language") == "vhdl": raise ScopeError("The intent layer is SystemVerilog-only; " "VHDL beta sessions do not support load_intent.") - if naja.intent_available(): + if naja.intentAvailable(): return self.intent spec = self.load_spec or {} files = files if files is not None else spec.get("files") @@ -130,6 +135,8 @@ def load_intent(self, flist: Optional[str] = None, top = top if top is not None else spec.get("top") defines = spec.get("defines") allow_unknown_designs = spec.get("allow_unknown_designs", False) + blackbox_multi_writer_memories = spec.get( + "blackbox_multi_writer_memories", False) if not files and not flist: raise ScopeError( "load_intent needs a flist or files to elaborate the intent " @@ -143,12 +150,13 @@ def load_intent(self, flist: Optional[str] = None, self.__init__() self.load_systemverilog(files or [], flist=flist, top=top, keep_ast_link=True, defines=defines, - allow_unknown_designs=allow_unknown_designs) + allow_unknown_designs=allow_unknown_designs, + blackbox_multi_writer_memories=blackbox_multi_writer_memories) return self.intent @property def intent_available(self) -> bool: - return bool(naja.intent_available()) + return bool(naja.intentAvailable()) # -- source resolution --------------------------------------------------- diff --git a/src/naja_scope/snl.py b/src/naja_scope/snl.py index ff100b1..5229611 100644 --- a/src/naja_scope/snl.py +++ b/src/naja_scope/snl.py @@ -10,7 +10,7 @@ This module centralises every raw-SNL primitive the tools need: * universe / top / design enumeration, -* source locations (getSourceLoc), +* source locations (getSourceDeclaration / getSourceOrigins / getSourceLoc), * `InstNode` — a hierarchical instance occurrence carrying both a name-path (for display) and an `SNLPath` (for occurrence/equipotential construction), * term/net bit handling, @@ -98,24 +98,68 @@ def suggest_designs(name: str, n: int = 8) -> List[str]: # -- source locations -------------------------------------------------------- -def source_loc(obj) -> Optional[Tuple[str, int, int, int, int]]: - """Normalised (file, line, end_line, column, end_column), or None. +Loc = Tuple[str, int, int, int, int] - The raw getSourceLoc tuple is (file, line, column, end_line, end_column); - we reorder so the line span leads, matching SrcRange. - """ + +def _norm(raw) -> Optional[Loc]: + """Raw (file, line, column, end_line, end_column) -> normalised Loc. + + We reorder so the line span leads, matching SrcRange.""" + if not raw or not raw[0]: + return None + file, line, column, end_line, end_column = raw + return (file, line, end_line, column, end_column) + + +def _source_refs(obj) -> Tuple[Optional[Loc], List[Loc]]: + """(declaration, origins) from najaeda>=0.7.29's language-neutral source + references. Each raw reference is ((file, line, col, end_line, end_col), + provider, language, representation).""" + if obj is None or not hasattr(obj, "getSourceDeclaration"): + return None, [] + try: + decl = obj.getSourceDeclaration() + origins = obj.getSourceOrigins() + except Exception: + return None, [] + decl_loc = _norm(decl[0]) if decl else None + origin_locs = [loc for loc in (_norm(o[0]) for o in origins or []) if loc] + return decl_loc, origin_locs + + +def _legacy_loc(obj) -> Optional[Loc]: if obj is None or not hasattr(obj, "hasSourceLoc"): return None try: if not obj.hasSourceLoc(): return None - loc = obj.getSourceLoc() + return _norm(obj.getSourceLoc()) except Exception: return None - if not loc: - return None - file, line, column, end_line, end_column = loc - return (file, line, end_line, column, end_column) + + +def source_loc(obj) -> Optional[Loc]: + """Normalised (file, line, end_line, column, end_column), or None. + + Prefers the source declaration (populated for SystemVerilog *and* + gate-level Verilog netlists), then the legacy getSourceLoc, then the first + source origin (SV cells inferred from a process carry only an origin). + """ + decl, origins = _source_refs(obj) + return decl or _legacy_loc(obj) or (origins[0] if origins else None) + + +def source_origins(obj) -> List[Loc]: + """Upstream source origins beyond source_loc(obj) — e.g. the RTL lines a + Yosys `(* src = ... *)` attribute points back to from a synthesized + netlist (load_verilog with yosys_source_attributes=True).""" + _decl, origins = _source_refs(obj) + primary = source_loc(obj) + out: List[Loc] = [] + for loc in origins: + if loc != primary and loc not in out: + out.append(loc) + return out def direction_str(direction) -> str: @@ -263,6 +307,9 @@ def child_count(self) -> int: def source_loc(self): return source_loc(self.snl_instance if not self.is_top else self.design) + def source_origins(self): + return source_origins(self.snl_instance if not self.is_top else self.design) + def top_node() -> InstNode: d = top_design() diff --git a/tests/test_intent.py b/tests/test_intent.py index d6390bb..85ebd35 100644 --- a/tests/test_intent.py +++ b/tests/test_intent.py @@ -154,7 +154,7 @@ def test_unknown_package_rejected(ip): def test_api_get_intent_degrades_when_unloaded(monkeypatch): from naja_scope import session as sess - monkeypatch.setattr(sess.naja, "intent_available", lambda: False) + monkeypatch.setattr(sess.naja, "intentAvailable", lambda: False) out = api.get_intent("anything") assert out["intent_loaded"] is False assert "get_source" in out["note"] @@ -188,7 +188,7 @@ def test_attach_intent_explicit_raises_env_swallows(monkeypatch): # effort), never failing the load. from naja_scope.session import SESSION from naja_scope import session as sess - monkeypatch.setattr(sess.naja, "intent_available", lambda: False) + monkeypatch.setattr(sess.naja, "intentAvailable", lambda: False) saved = SESSION.load_spec SESSION.load_spec = {} try: diff --git a/tests/test_net_types.py b/tests/test_net_types.py index 9613884..f5c04e5 100644 --- a/tests/test_net_types.py +++ b/tests/test_net_types.py @@ -19,7 +19,7 @@ def net_types_session(): def test_runtime_version_matches_release(): - assert __version__ == "0.1.18" + assert __version__ == "0.1.19" @pytest.mark.parametrize( diff --git a/tests/test_netlist_source.py b/tests/test_netlist_source.py new file mode 100644 index 0000000..6d5230b --- /dev/null +++ b/tests/test_netlist_source.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Source references on gate-level Verilog (najaeda>=0.7.29). + +Netlist objects carry a language-neutral source declaration (their line in +the .v file), and with yosys_source_attributes=True the Yosys +`(* src = ... *)` attributes become source origins pointing back to the RTL. +""" +import os + +import pytest + +from naja_scope import api +from naja_scope.session import SESSION + +HERE = os.path.dirname(__file__) +LIB = os.path.join(HERE, "..", "examples", "stdcells.lib") + +NETLIST = """(* src = "rtl.sv:1.1-9.10" *) +module top(input a, input clk, output y); + (* src = "rtl.sv:4.9-4.13|rtl.sv:6.3-6.20" *) + wire n; + (* src = "rtl.sv:5.3-7.6" *) + INV u_inv (.I(a), .Z(n)); + DFF u_ff (.D(n), .CK(clk), .Q(y)); +endmodule +""" + +RTL = "".join(f"// rtl line {i}\n" for i in range(1, 11)) + + +@pytest.fixture +def yosys_netlist(tmp_path): + (tmp_path / "rtl.sv").write_text(RTL) + path = tmp_path / "netlist.v" + path.write_text(NETLIST) + yield str(path) + SESSION.reset() + + +def _load(path, **kwargs): + SESSION.reset() + api.load_liberty([LIB]) + api.load_verilog([path], **kwargs) + + +def test_netlist_declaration_source(yosys_netlist): + _load(yosys_netlist) + out = api.get_source("top.u_inv", context_lines=0) + assert out["file"] == yosys_netlist + assert out["start"] == 6 + assert "INV u_inv" in out["text"] + assert "origins" not in out + assert api.resolve("top.u_inv")["matches"][0]["src"].endswith("netlist.v:6") + + +def test_yosys_origins_cross_probe_to_rtl(yosys_netlist, tmp_path): + _load(yosys_netlist, yosys_source_attributes=True) + match = api.resolve("top.u_inv")["matches"][0] + assert match["origins"] == ["rtl.sv:5-7"] + out = api.get_source("top.u_inv", context_lines=0, origin=0) + assert out["origin"] == 0 + assert out["file"] == str(tmp_path / "rtl.sv") + assert out["text"] == "// rtl line 5\n// rtl line 6\n// rtl line 7\n" + # Multi-location src attributes are split into separate origins. + assert api.resolve("top.n")["matches"][0]["origins"] == [ + "rtl.sv:4", "rtl.sv:6"] + # Cells without a src attribute have none; bad indexes are structured. + assert "origins" not in api.resolve("top.u_ff")["matches"][0] + assert "error" in api.get_source("top.u_ff", origin=0) + + +def test_yosys_origins_survive_snapshot(yosys_netlist, tmp_path): + _load(yosys_netlist, yosys_source_attributes=True) + snap = str(tmp_path / "snap") + os.makedirs(snap) + api.save_snapshot(snap) + api.reset_universe() + api.load_snapshot(snap) + match = api.resolve("top.u_inv")["matches"][0] + assert match["src"].endswith("netlist.v:6") + assert match["origins"] == ["rtl.sv:5-7"] diff --git a/tests/test_sv_load_options.py b/tests/test_sv_load_options.py index 77ea757..888eb75 100644 --- a/tests/test_sv_load_options.py +++ b/tests/test_sv_load_options.py @@ -46,3 +46,44 @@ def test_defines_select_ifdef_branch(tmp_path): card = api.get_module_card("top") reg_models = card["counts"]["by_model"] assert any("8" in name or "w8" in name for name in reg_models), reg_models + + +_DP_RAM = """module dp_ram(input logic clk_a, clk_b, we_a, we_b, + input logic [2:0] addr_a, addr_b, + input logic [7:0] wd_a, wd_b, + output logic [7:0] rd_a, rd_b); + logic [7:0] mem [8]; + always @(posedge clk_a) begin + if (we_a) mem[addr_a] = wd_a; + rd_a <= mem[addr_a]; + end + always @(posedge clk_b) begin + if (we_b) mem[addr_b] = wd_b; + rd_b <= mem[addr_b]; + end +endmodule +module top(input logic clk_a, clk_b, we_a, we_b, + input logic [2:0] addr_a, addr_b, + input logic [7:0] wd_a, wd_b, + output logic [7:0] rd_a, rd_b); + dp_ram ram(.*); +endmodule +""" + + +def test_blackbox_multi_writer_memories(tmp_path): + """najaeda>=0.7.29 can blackbox a module whose memory has several + writers (unsupported inference) instead of failing the whole load.""" + (tmp_path / "dp_ram.sv").write_text(_DP_RAM) + source = str(tmp_path / "dp_ram.sv") + SESSION.reset() + with pytest.raises(ScopeError): + api.load_systemverilog(files=[source]) + SESSION.reset() + res = api.load_systemverilog(files=[source], + blackbox_multi_writer_memories=True) + assert res["top"]["name"] == "top" + ram = api.resolve("top.ram")["matches"][0] + assert ram["model"] == "dp_ram" + assert ram["is_leaf"] is True + assert SESSION.load_spec["blackbox_multi_writer_memories"] is True diff --git a/tests/test_vhdl_load.py b/tests/test_vhdl_load.py index ae66027..52afbcd 100644 --- a/tests/test_vhdl_load.py +++ b/tests/test_vhdl_load.py @@ -60,6 +60,39 @@ def test_package_then_top(tmp_path, vhdl_file): assert api.status()["loaded_files"] == [str(package), vhdl_file] +def test_batch_files_any_order(tmp_path): + child = tmp_path / "child.vhd" + child.write_text(""" +library ieee; +use ieee.std_logic_1164.all; +entity child is + port (a : in std_logic; y : out std_logic); +end entity; +architecture rtl of child is +begin + y <= a; +end architecture; +""") + top = tmp_path / "top.vhd" + top.write_text(""" +library ieee; +use ieee.std_logic_1164.all; +entity top is + port (a : in std_logic; y : out std_logic); +end entity; +architecture rtl of top is +begin + u : entity work.child port map (a => a, y => y); +end architecture; +""") + # The top's user precedes the child's definition: units are resolved. + with pytest.warns(RuntimeWarning, match="Beta"): + result = api.load_vhdl([str(top), str(child)]) + assert result["top"]["model"] == "top" + assert api.resolve("top.u")["matches"][0]["model"] == "child" + assert api.status()["loaded_files"] == [str(top), str(child)] + + def test_snapshot_preserves_vhdl_intent_guard(tmp_path, vhdl_file, monkeypatch): with pytest.warns(RuntimeWarning, match="Beta"): api.load_vhdl(vhdl_file) From dcd00c110ba6c69990044f3c0e86fb24791dc0c5 Mon Sep 17 00:00:00 2001 From: xtof Date: Tue, 13 Oct 2026 11:56:37 +0200 Subject: [PATCH 2/3] Require naja-schematic 0.2 and pass source roots to the viewer naja-schematic 0.2.0 requires najaeda>=0.7.29, drops the deprecated is_clock() call, and adds Show Sources; pass the session's source directories as its source_roots. --- README.md | 2 +- README_PyPI.md | 2 +- pyproject.toml | 2 +- src/naja_scope/schematic.py | 7 +++++-- tests/test_schematic.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b3767ec..d1b4bdf 100644 --- a/README.md +++ b/README.md @@ -277,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 diff --git a/README_PyPI.md b/README_PyPI.md index 2c61c4c..aab6f8d 100644 --- a/README_PyPI.md +++ b/README_PyPI.md @@ -212,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 diff --git a/pyproject.toml b/pyproject.toml index c47004e..d450c3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ ] [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" diff --git a/src/naja_scope/schematic.py b/src/naja_scope/schematic.py index b6e2c4a..f08101e 100644 --- a/src/naja_scope/schematic.py +++ b/src/naja_scope/schematic.py @@ -20,9 +20,12 @@ def _server(): try: from naja_schematic import ViewerServer except ImportError as exc: - raise ScopeError('Schematic tools require naja-schematic>=0.1.6; ' + raise ScopeError('Schematic tools require naja-schematic>=0.2.0; ' 'install "naja-scope[schematic]".') from exc - viewer = ViewerServer(lock=DESIGN_LOCK) + # naja-schematic>=0.2 resolves relative source references (Show + # Sources) against these roots; they are fixed at viewer creation. + viewer = ViewerServer(lock=DESIGN_LOCK, + source_roots=tuple(SESSION.source_dirs)) try: viewer.start() except (OSError, RuntimeError) as exc: diff --git a/tests/test_schematic.py b/tests/test_schematic.py index 90ba96f..b65e1d1 100644 --- a/tests/test_schematic.py +++ b/tests/test_schematic.py @@ -39,7 +39,7 @@ def design(): def viewer(design): package = pytest.importorskip("naja_schematic") if not hasattr(package, "ViewerServer"): - pytest.skip("requires naja-schematic>=0.1.6") + pytest.skip("requires naja-schematic>=0.2.0") pytest.importorskip("websockets") return design From 5d2e057c8a4abc345dce1135c58ed277cd257bdd Mon Sep 17 00:00:00 2001 From: xtof Date: Tue, 13 Oct 2026 13:02:27 +0200 Subject: [PATCH 3/3] Assert the precise noc_req_o cone in the CVA6 demo najaeda 0.7.29's memory inference no longer lets the cone run through the AXI adapter's registered (non-fall-through) I/D-cache data FIFOs. The fan-in of cva6.noc_req_o now stops at 15 registers inside cache_subsystem.i_adapter (16,145 nodes); 0.7.27 over-approximated it at ~2.47M nodes reaching back into the caches. Assert that the frontier lives in the adapter instead of requiring a >100k-node cone. --- examples/README.md | 5 +++-- examples/cva6_demo.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 73a2081..7e88b9f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/cva6_demo.py b/examples/cva6_demo.py index 32df60e..2f47b33 100755 --- a/examples/cva6_demo.py +++ b/examples/cva6_demo.py @@ -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 "