From 5179b0b8811377c8c56b6a16f0cda517c93a4f9f Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 18 Oct 2026 16:05:29 +0200 Subject: [PATCH 1/2] Prepare naja-scope 0.1.20 with najaeda 0.7.30 storage summaries --- .github/workflows/ci.yml | 2 +- CLAUDE.md | 20 ++-- README.md | 18 ++++ README_PyPI.md | 16 +++ pyproject.toml | 2 +- server.json | 4 +- src/naja_scope/__init__.py | 2 +- src/naja_scope/api.py | 18 +++- src/naja_scope/cards.py | 27 +++-- src/naja_scope/loader.py | 2 +- src/naja_scope/server.py | 14 ++- src/naja_scope/storage.py | 84 +++++++++++++++ tests/test_net_types.py | 2 +- tests/test_storage.py | 202 +++++++++++++++++++++++++++++++++++++ 14 files changed, 379 insertions(+), 34 deletions(-) create mode 100644 src/naja_scope/storage.py create mode 100644 tests/test_storage.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93d1cce..5fc998d 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.29" + python -m pip install --no-binary=najaeda "najaeda>=0.7.30" - name: Install package + test dependencies from wheels if: ${{ !matrix.najaeda_source_build }} diff --git a/CLAUDE.md b/CLAUDE.md index 54c6403..89f727c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index d1b4bdf..40bfd5c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_PyPI.md b/README_PyPI.md index aab6f8d..03652aa 100644 --- a/README_PyPI.md +++ b/README_PyPI.md @@ -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. diff --git a/pyproject.toml b/pyproject.toml index d450c3e..4f1cef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "najaeda>=0.7.29", + "najaeda>=0.7.30", "mcp>=1.28,<2", ] diff --git a/server.json b/server.json index 45d317a..65de049 100644 --- a/server.json +++ b/server.json @@ -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" } diff --git a/src/naja_scope/__init__.py b/src/naja_scope/__init__.py index 64e5952..e9c073c 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.19" +__version__ = "0.1.20" diff --git a/src/naja_scope/api.py b/src/naja_scope/api.py index c7cb7ce..885464f 100644 --- a/src/naja_scope/api.py +++ b/src/naja_scope/api.py @@ -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, @@ -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 = { @@ -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(): @@ -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 @@ -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( @@ -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, diff --git a/src/naja_scope/cards.py b/src/naja_scope/cards.py index 8b2ae45..696fc2e 100644 --- a/src/naja_scope/cards.py +++ b/src/naja_scope/cards.py @@ -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 @@ -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()): @@ -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`): @@ -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 @@ -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 @@ -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, @@ -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"], diff --git a/src/naja_scope/loader.py b/src/naja_scope/loader.py index bedb4d6..e8a4fab 100644 --- a/src/naja_scope/loader.py +++ b/src/naja_scope/loader.py @@ -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 diff --git a/src/naja_scope/server.py b/src/naja_scope/server.py index 5494983..3e1290f 100644 --- a/src/naja_scope/server.py +++ b/src/naja_scope/server.py @@ -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) @@ -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) diff --git a/src/naja_scope/storage.py b/src/naja_scope/storage.py new file mode 100644 index 0000000..8ab90a5 --- /dev/null +++ b/src/naja_scope/storage.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Storage facts from raw sequential models and native DB0 memory primitives.""" +from __future__ import annotations + +from . import snl + + +def model_key(model): + lib = model.getLibrary() + return (lib.getDB().getID(), lib.getID(), model.getID()) + + +def memory_info(instance): + model = instance.getModel() + lib = model.getLibrary() + # NLDB0::isMemory uses the reserved DB0 MEMORY library. Do not classify + # user modules by a suggestive name or WIDTH/DEPTH parameters alone. + if not (model.isPrimitive() and lib.getDB().getID() == 0 + and lib.getName() == 'MEMORY'): + return None + + def value(name): + param = instance.getInstParameter(name) + if param is None: + param = model.getParameter(name) + return param.getValue() if param is not None else None + + width, depth = int(value('WIDTH')), int(value('DEPTH')) + writes = int(value('WR_PORTS')) + initialized = bool(int(value('INIT_ENABLE'))) + info = { + 'instance': snl.inst_segment(instance), + 'model': model.getName(), + 'kind': 'rom' if writes == 0 else 'ram', + 'width': width, 'depth': depth, 'bits': width * depth, + 'read_ports': int(value('RD_PORTS')), 'write_ports': writes, + 'initialized': initialized, + } + if initialized: + image = value('INIT') + info['init_preview'] = image[:64] + info['init_truncated'] = len(image) > 64 + return info + + +def register_bits(instance, static_cache): + """Resolved flip-flop/latch states; None means no usable model. + + Call only for non-memory sequential cells. Instance resolution honors + parameter-dependent models, rather than counting outputs (Q/QN alias). + """ + model = instance.getModel() + if model.hasSequentialModelFromParameters(): + resolved = instance.getSequentialModel() + return len(resolved['states']) + key = model_key(model) + if key not in static_cache: + static_cache[key] = (len(model.getSequentialModel()['states']) + if model.hasSequentialModel() else None) + return static_cache[key] + + +COUNT_FIELDS = ('register_bits', 'register_bits_unknown_instances', + 'memory_instances', 'rom_instances', 'ram_instances', 'memory_bits') + + +def empty_counts(): + return dict.fromkeys(COUNT_FIELDS, 0) + + +def add_instance(counts, instance, static_cache): + """Accumulate one direct child's storage; return memory detail if present.""" + memory = memory_info(instance) + if memory is not None: + counts['memory_instances'] += 1 + counts[memory['kind'] + '_instances'] += 1 + counts['memory_bits'] += memory['bits'] + elif instance.getModel().isSequential(): + bits = register_bits(instance, static_cache) + if bits is None: + counts['register_bits_unknown_instances'] += 1 + else: + counts['register_bits'] += bits + return memory diff --git a/tests/test_net_types.py b/tests/test_net_types.py index f5c04e5..84ade83 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.19" + assert __version__ == "0.1.20" @pytest.mark.parametrize( diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..f892a3d --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,202 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Storage counts use state models and native memories, including snapshots.""" +import json + +import pytest +from najaeda import naja + +from naja_scope import api +from naja_scope.session import SESSION + +SV = """ +module store(input clk, we, input [1:0] a, input [7:0] d, + output [7:0] q, r, output reg [4:0] regq); + logic [7:0] mem [0:3]; + localparam logic [7:0] rom [0:3] = '{8'h12,8'h34,8'h56,8'h78}; + always_ff @(posedge clk) begin + if (we) mem[a] <= d; + regq <= d[4:0]; + end + assign q = mem[a]; + assign r = rom[a]; +endmodule +module top(input clk, we, input [1:0] a, input [7:0] d, + output [7:0] q0, q1, r0, r1, output [4:0] s0, s1); + store u0(clk, we, a, d, q0, r0, s0); + store u1(clk, we, a, d, q1, r1, s1); +endmodule +""" + +LIB = """ +library(storage_test) { + cell(PAIR) { + ff_bank(S, SN, 2) { clocked_on : "CK"; next_state : "D"; } + pin(CK) { direction : input; clock : true; } + bundle(D) { + members(D0, D1); direction : input; + pin(D0) {} pin(D1) {} + } + bundle(Q) { + members(Q0, Q1); direction : output; function : "S"; + pin(Q0) {} pin(Q1) {} + } + pin(QN0) { direction : output; function : "!S"; } + } +} +""" + + +@pytest.fixture(autouse=True) +def fresh_session(): + SESSION.reset() + yield + SESSION.reset() + + +def load_storage(tmp_path): + source = tmp_path / 'storage.sv' + source.write_text(SV) + api.load_systemverilog([str(source)], top='top') + + +def test_memory_and_register_counts(tmp_path): + load_storage(tmp_path) + card = api.get_module_card('store') + counts = card['counts'] + assert counts['register_bits'] == 5 + assert counts['register_bits_unknown_instances'] == 0 + assert counts['memory_instances'] == 2 + assert counts['memory_bits'] == 64 + assert counts['rom_instances'] == counts['ram_instances'] == 1 + rom = next(m for m in card['memories'] if m['kind'] == 'rom') + ram = next(m for m in card['memories'] if m['kind'] == 'ram') + assert (rom['width'], rom['depth'], rom['read_ports'], rom['write_ports']) == (8, 4, 1, 0) + assert rom['initialized'] is True + assert rom['init_preview'] == "32'h78563412" + assert rom['init_truncated'] is False + assert ram['initialized'] is False + assert ram['write_ports'] == 1 + assert 'init_preview' not in ram + assert card['memories_truncated'] is False + # Direct-child counts differ from totals across both instances of store. + assert api.get_module_card('top')['counts']['memory_bits'] == 0 + stats = api.get_stats(limit=1) + assert stats['flat_register_bits'] == 10 + assert stats['flat_memory_bits'] == 128 + assert stats['flat_memory_instances'] == 4 + assert stats['flat_rom_instances'] == stats['flat_ram_instances'] == 2 + assert stats['flat_register_bits_unknown_instances'] == 0 + assert stats['has_more'] is True + subtree = api.get_stats('top.u0') + assert subtree['flat_register_bits'] == 5 + assert subtree['flat_memory_bits'] == 64 + snapshot = tmp_path / 'snapshot' + snapshot.mkdir() + api.save_snapshot(str(snapshot)) + SESSION.reset() + api.load_snapshot(str(snapshot)) + assert api.get_module_card('store') == card + assert api.get_stats(limit=1) == stats + json.dumps(card) # The public result contains no native objects. + + +def test_memory_details_are_bounded(tmp_path): + load_storage(tmp_path) + design = SESSION.require_top().design.getInstance('u0').getModel() + rom = next(i for i in design.getInstances() + if i.getModel().getParameter('WR_PORTS') is not None + and i.getModel().getParameter('WR_PORTS').getValue() == '0') + # An instance override must win over the primitive's default INIT. + image = "32'b" + ('1_0_x_z_' * 8).rstrip('_') + rom.getInstParameter('INIT').setValue(image) + for index in range(13): + naja.SNLInstance.create(design, rom.getModel(), f'extra_rom_{index}') + card = api.get_module_card('store') + assert len(card['memories']) == 12 + assert card['counts']['memory_instances'] == 15 + assert card['memories_truncated'] is True + detail = next(m for m in card['memories'] if m['initialized']) + assert detail['init_preview'] == image[:64] + assert detail['init_truncated'] is True + + +def test_liberty_bank_counts_states_not_outputs(tmp_path): + lib = tmp_path / 'bank.lib' + lib.write_text(LIB) + source = tmp_path / 'top.v' + source.write_text('''module top(input clk, a, b, output q0, q1); + PAIR u_bank(.CK(clk), .D0(a), .D1(b), .Q0(q0), .Q1(q1)); + endmodule''') + api.load_liberty([str(lib)]) + api.load_verilog([str(source)]) + card = api.get_module_card('top') + assert card['counts']['sequential_instances'] == 1 + assert card['counts']['register_bits'] == 2 + assert card['counts']['register_bits_unknown_instances'] == 0 + assert card['counts']['memory_instances'] == 0 + assert api.get_stats()['flat_register_bits'] == 2 + + +def test_vhdl_rom(tmp_path): + source = tmp_path / 'rom.vhd' + source.write_text('''library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +entity rom_top is + port(a : in unsigned(1 downto 0); q : out std_logic_vector(7 downto 0)); +end entity; +architecture rtl of rom_top is + type words is array(0 to 3) of std_logic_vector(7 downto 0); + constant table_data : words := (x"12", x"34", x"56", x"78"); +begin + q <= table_data(to_integer(a)); +end architecture; +''') + with pytest.warns(RuntimeWarning, match='Beta'): + api.load_vhdl(str(source), top='rom_top') + card = api.get_module_card('rom_top') + assert card['counts']['rom_instances'] == 1 + assert card['counts']['memory_bits'] == 32 + assert card['counts']['register_bits'] == 0 + assert card['memories'][0]['init_preview'] == "32'h78563412" + + +def test_sequential_cell_without_state_model_is_unknown(tmp_path): + load_storage(tmp_path) + top = SESSION.require_top().design + lib = naja.NLLibrary.createPrimitives(top.getLibrary().getDB()) + model = naja.SNLDesign.createPrimitive(lib, 'timing_only') + clock = naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Input, 'CK') + data = naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Input, 'D') + model.addInputsToClockArcs([data], clock) + naja.SNLInstance.create(top, model, 'unknown_reg') + counts = api.get_module_card('top')['counts'] + assert counts['sequential_instances'] == 1 + assert counts['register_bits'] == 0 + assert counts['register_bits_unknown_instances'] == 1 + stats = api.get_stats() + assert stats['flat_register_bits'] == 10 + assert stats['flat_register_bits_unknown_instances'] == 1 + + +def test_parameter_dependent_register_states(tmp_path): + load_storage(tmp_path) + top = SESSION.require_top().design + lib = naja.NLLibrary.createPrimitives(top.getLibrary().getDB()) + model = naja.SNLDesign.createPrimitive(lib, 'configurable_reg') + for name in ('CK', 'D'): + naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Input, name) + output = naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Output, 'Q') + param = naja.SNLParameter.createBinary(model, 'DOUBLE', 1, 0) + model.setSequentialModelFromParameters(parameters=['DOUBLE'], models=[ + {'values': [0], 'clocked_on': 'CK', + 'states': [{'name': 'S', 'next_state': 'D'}], 'outputs': [(output, 'S')]}, + {'values': [1], 'clocked_on': 'CK', + 'states': [{'name': 'S', 'next_state': 'D'}, + {'name': 'T', 'next_state': 'S'}], 'outputs': [(output, 'T')]}, + ]) + naja.SNLInstance.create(top, model, 'single') + double = naja.SNLInstance.create(top, model, 'double') + naja.SNLInstParameter.create(double, param, "1'b1") + assert api.get_module_card('top')['counts']['register_bits'] == 3 + assert api.get_stats()['flat_register_bits'] == 13 From cad9e3cc238dd573dd6bb092aa4531eeb07ec729 Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 18 Oct 2026 16:20:20 +0200 Subject: [PATCH 2/2] Revalidate CORE-V-MCU demo baseline with najaeda 0.7.30 --- examples/core_v_mcu_demo.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/core_v_mcu_demo.py b/examples/core_v_mcu_demo.py index efc65ec..0107380 100755 --- a/examples/core_v_mcu_demo.py +++ b/examples/core_v_mcu_demo.py @@ -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"