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.29"
python -m pip install --no-binary=najaeda "najaeda>=0.7.30"

- name: Install package + test dependencies from wheels
if: ${{ !matrix.najaeda_source_build }}
Expand Down
20 changes: 11 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,30 @@ 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-13, the verified local regression baseline is CVA6 v5.3.0
As of 2026-10-18, the verified local regression baseline is CVA6 v5.3.0
(`2ef1c1b1fca419354920c5487293bc605294904e`), configuration
`cv32a6_imac_sv32`, with najaeda 0.7.29. All three tests in
`cv32a6_imac_sv32`, with najaeda 0.7.30. All three tests in
`tests/test_zzz_cone_cva6.py` and `tests/test_zzz_hierarchy_cva6.py` pass
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.
0.7.30.

The latest upstream release remains v5.3.0. Upstream master
`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`
retried on 2026-10-18 in an isolated checkout with its pinned submodules and
najaeda 0.7.30. The `cv32a6_imac_sv32` raw-API elaboration still fails with
3 unsupported elements: the MMU's `always_comb`
condition contains an `InvalidExpression` at `core/cva6_mmu/cva6_mmu.sv:404`,
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 (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
the reversed Sv32 part-select, remains open (two comments; the contributor
reports a local fix and passing Slang elaboration for Sv32 and two RV64
configurations, with formatting and final review pending before PR
submission). 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
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,24 @@ example lives in [`examples/`](examples/) (`stdcells.lib` + `counter2.v` +

---

## Storage summaries

`get_module_card` reports direct-child register bits and inferred RAM/ROM
summaries: width, depth, capacity in bits, read/write port counts, and
initialization status. It lists up to 12 memories, with `memories_truncated`
when more exist; initialized memories include a 64-character `init_preview`
and `init_truncated`. Address zero occupies the least-significant word.

`get_stats` aggregates these counts below the selected instance using
`flat_register_bits`, `flat_memory_instances`, `flat_rom_instances`,
`flat_ram_instances`, and `flat_memory_bits`. Register bits count modeled
flip-flop/latch states, including multibit Liberty banks; RAM capacity is
separate. Sequential cells without a state model are counted in
`register_bits_unknown_instances` (`flat_` in statistics), so known bit
counts remain explicit lower bounds when that count is nonzero. Memories
are recognized from native naja primitives; opaque memory macros are not
assigned an inferred capacity.

## VHDL (beta)

VHDL loading is available in beta. Call
Expand Down
16 changes: 16 additions & 0 deletions README_PyPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ or a post-synthesis structural Verilog netlist plus its Liberty standard-cell
library (see [Gate-level designs](#gate-level-designs)). VHDL loading is in
[beta](#vhdl-beta).

`get_module_card` reports direct-child register bits and inferred RAM/ROM
summaries: width, depth, capacity in bits, read/write port counts, and
initialization status. It lists up to 12 memories, with `memories_truncated`
when more exist; initialized memories include a 64-character `init_preview`
and `init_truncated`. Address zero occupies the least-significant word.

`get_stats` aggregates these counts below the selected instance using
`flat_register_bits`, `flat_memory_instances`, `flat_rom_instances`,
`flat_ram_instances`, and `flat_memory_bits`. Register bits count modeled
flip-flop/latch states, including multibit Liberty banks; RAM capacity is
separate. Sequential cells without a state model are counted in
`register_bits_unknown_instances` (`flat_` in statistics), so known bit
counts remain explicit lower bounds when that count is nonzero. Memories
are recognized from native naja primitives; opaque memory macros are not
assigned an inferred capacity.

All responses are token-bounded: lists paginate, large results truncate with
clear markers. Your context stays small; your answers stay accurate.

Expand Down
14 changes: 7 additions & 7 deletions examples/core_v_mcu_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +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.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"
# Revalidated on the pinned CORE-V-MCU commit with najaeda 0.7.30:
# 630,239 leaves (631,603 with 0.7.29). A same-source comparison shows
# changed GPIO/arbiter/FIFO lowering and a table-select replaced by a ROM;
# sequential cell count, hierarchy model count, blackboxes, and CDC fanout
# remain unchanged. These count native instances, including word-wide
# cells, rather than RTL bits.
assert root["flat_leaves"] == 630_239, "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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ classifiers = [
"Topic :: Software Development :: Libraries",
]
dependencies = [
"najaeda>=0.7.29",
"najaeda>=0.7.30",
"mcp>=1.28,<2",
]

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.19",
"version": "0.1.20",
"packages": [
{
"registryType": "pypi",
"identifier": "naja-scope",
"version": "0.1.19",
"version": "0.1.20",
"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.19"
__version__ = "0.1.20"
18 changes: 13 additions & 5 deletions src/naja_scope/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from . import cone as cone_mod
from . import connectivity
from . import loader
from . import snl
from . import snl, storage
from .errors import ScopeError
from .paging import clamp_limit, paginate
from .resolve import (MAX_ORIGINS, Resolved, describe, resolve_path,
Expand Down Expand Up @@ -440,11 +440,11 @@ def _safe_seq(model) -> bool:
return False


def _collect_model_stats(design, memo: dict) -> dict:
def _collect_model_stats(design, memo: dict, sequential_models: dict) -> dict:
"""Hierarchical stats memoized per model. Deliberately avoids
najaeda.stats: its is_basic_primitive crashes the process on multi-output
primitives like naja_fa."""
model_id = design.getID()
model_id = storage.model_key(design)
if model_id in memo:
return memo[model_id]
entry = {
Expand All @@ -454,6 +454,7 @@ def _collect_model_stats(design, memo: dict) -> dict:
"children_by_model": {},
"flat_leaves": 0,
"flat_sequential": 0,
**{"flat_" + field: 0 for field in storage.COUNT_FIELDS},
}
memo[model_id] = entry
for child in design.getInstances():
Expand All @@ -465,15 +466,21 @@ def _collect_model_stats(design, memo: dict) -> dict:
entry["leaf_by_model"][name] = (
entry["leaf_by_model"].get(name, 0) + 1)
entry["flat_leaves"] += 1
counts = storage.empty_counts()
storage.add_instance(counts, child, sequential_models)
for field, value in counts.items():
entry["flat_" + field] += value
if _safe_seq(model):
entry["flat_sequential"] += 1
else:
sub = _collect_model_stats(model, memo)
sub = _collect_model_stats(model, memo, sequential_models)
name = _model_label(model.getName())
entry["children_by_model"][name] = (
entry["children_by_model"].get(name, 0) + 1)
entry["flat_leaves"] += sub["flat_leaves"]
entry["flat_sequential"] += sub["flat_sequential"]
for field in storage.COUNT_FIELDS:
entry["flat_" + field] += sub["flat_" + field]
return entry


Expand All @@ -487,7 +494,7 @@ def get_stats(path: Optional[str] = None, limit: Optional[int] = None,
else:
design = SESSION.require_top().design
memo: dict = {}
root = _collect_model_stats(design, memo)
root = _collect_model_stats(design, memo, {})
models = sorted(memo.values(), key=lambda m: -m["flat_leaves"])
for m in models:
m["leaf_by_model"] = dict(sorted(
Expand All @@ -497,6 +504,7 @@ def get_stats(path: Optional[str] = None, limit: Optional[int] = None,
"root_model": root["model"],
"flat_leaves": root["flat_leaves"],
"flat_sequential": root["flat_sequential"],
**{"flat_" + field: root["flat_" + field] for field in storage.COUNT_FIELDS},
"models": page,
"total_models": len(models),
**envelope,
Expand Down
27 changes: 19 additions & 8 deletions src/naja_scope/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import re
from typing import Dict, List, Optional

from . import snl
from . import snl, storage
from .errors import ScopeError
from .session import Session
from .source_index import SrcRange
Expand Down Expand Up @@ -115,10 +115,7 @@ def _model_is_sequential(model) -> bool:

def _model_role_facts(model) -> dict:
"""Primitive pin-role facts for a leaf model (SNLDesign.getClockTerms()
etc.), sound (not name-guessed) but only populated for primitives
elaborated from RTL via slang -- empty for hierarchical models and for
primitives sourced from yosys/xilinx/liberty (role porting there is
upstream WIP as of najaeda 0.7.12)."""
etc.), sound (not name-guessed) for modeled RTL and library primitives."""
facts = {}
try:
if list(model.getAsyncResetTerms()):
Expand Down Expand Up @@ -156,9 +153,14 @@ def module_card(session: Session, module: str,
count, with `by_model_truncated` if elided). Hard fact --
sequential_with_(a)sync_* comes from each leaf model's own
SNLDesign.get(A)SyncResetTerms()/get(A)SyncSetTerms() (sound, primitive
pin roles), not name matching. Only populated for primitives
elaborated from RTL via slang; 0 for gate-level netlists sourced from
yosys/xilinx/liberty until that role porting lands upstream.
pin roles), not name matching.
- Storage counts describe direct children: register_bits counts modeled
flip-flop/latch states (not Q/QN outputs), excluding memories.
register_bits_unknown_instances counts sequential non-memory cells
without a state model. memory_bits is width * depth for native DB0
RAM/ROM primitives. `memories` lists at most max_models such children,
with initialization previews capped at 64 characters; address zero is
the least-significant word of INIT.
- `parameters`: name/value pairs, when present. Hard fact.
- `src`: the module's source range as `file:start-end`, when known.
- `clock_candidates` / `reset_candidates` (+ `active_low_guess`):
Expand All @@ -172,6 +174,9 @@ def module_card(session: Session, module: str,

by_model: Dict[str, int] = {}
role_facts_by_model: Dict[str, dict] = {}
storage_counts = storage.empty_counts()
sequential_models = {}
memories = []
total = 0
sequential = 0
sequential_with_async_reset = 0
Expand All @@ -180,6 +185,9 @@ def module_card(session: Session, module: str,
sequential_with_sync_set = 0
for inst in design.getInstances():
total += 1
memory = storage.add_instance(storage_counts, inst, sequential_models)
if memory is not None and len(memories) < max_models:
memories.append(memory)
model = inst.getModel()
model_name = model.getName()
by_model[model_name] = by_model.get(model_name, 0) + 1
Expand All @@ -199,6 +207,7 @@ def module_card(session: Session, module: str,

top_models = sorted(by_model.items(), key=lambda kv: -kv[1])
counts = {
**storage_counts,
"instances": total,
"sequential_instances": sequential,
"sequential_with_async_reset": sequential_with_async_reset,
Expand Down Expand Up @@ -229,6 +238,8 @@ def module_card(session: Session, module: str,
"module": module,
"ports": ports,
"counts": counts,
"memories": memories,
"memories_truncated": storage_counts["memory_instances"] > len(memories),
"clock_candidates": clocks,
"reset_candidates": resets,
"heuristic_fields": ["clock_candidates", "reset_candidates"],
Expand Down
2 changes: 1 addition & 1 deletion src/naja_scope/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .errors import ScopeError, SVInternalError, SVSyntaxError, SVUnsupportedError

# najaeda>=0.7.9 introduced these typed exceptions; naja-scope currently
# requires najaeda>=0.7.29.
# requires najaeda>=0.7.30.
# 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
Expand Down
14 changes: 9 additions & 5 deletions src/naja_scope/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,9 @@ def get_module_card(
],
) -> dict:
"""Deterministic module summary: ports, instance counts by model,
sequential count, source ref, plus clock/reset candidates — a name-based
regex guess, not a structural result; verify before relying on it. Use this
sequential cell and register-bit counts, bounded RAM/ROM details (width,
depth, ports, initialization preview), and source ref. Clock/reset candidates
are name-based guesses; verify before relying on them. Use this
for one model's interface; use get_stats for counts below an instance."""
return api.get_module_card(module)

Expand All @@ -519,10 +520,13 @@ def get_stats(
] = None,
) -> dict:
"""Summarize instance population by model below `path` or the top design.
Use this for aggregate leaf/sequential/model counts; use get_hierarchy for
actual child instances or get_module_card for one model's ports. This
Use this for aggregate leaf/sequential/model counts, register bits, and
RAM/ROM counts and capacity; use get_hierarchy for actual child instances
or get_module_card for one model's ports. This
read-only query requires a loaded design. Returns `root_model`, flat totals,
a paginated `models` list, `total_models`, `has_more`, and `next_cursor`."""
a paginated `models` list, `total_models`, `has_more`, and `next_cursor`.
Register bits count modeled flip-flop/latch states, excluding memories;
unknown sequential bit counts are reported separately."""
return api.get_stats(path, limit, cursor)


Expand Down
Loading
Loading