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
111 changes: 93 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ concurrency:

defaults:
run:
# bash is available on all three GitHub-hosted runners (Git Bash on
# Windows), so the same step commands behave identically everywhere.
# Git Bash is available on the Windows runner, so all jobs can share the
# same commands.
shell: bash

jobs:
test:
name: test (${{ matrix.os }}, py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
python-versions:
name: Python ${{ matrix.python-version }} (Linux x86_64)
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
# Test on the three OSes naja-scope claims to support. najaeda ships
# wheels for each (macOS arm64, win_amd64, manylinux) on these Pythons.
# 3.15 is still a prerelease, hence -dev + allow-prereleases below.
os: [ubuntu-latest, macos-latest, windows-latest]
# naja-scope is pure Python, while najaeda publishes a wheel for each
# of these CPython versions. Keep language-version coverage separate
# from the native platform matrix to avoid a large Cartesian product.
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"]
steps:
- uses: actions/checkout@v7
Expand All @@ -38,23 +37,99 @@ jobs:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Install package + test deps
- name: Verify runner architecture
run: python ci/check_architecture.py x86_64

- name: Install package + test dependencies
run: |
python -m pip install --upgrade pip
# pulls najaeda>=0.7.24 (ships cp310..cp315 wheels) and mcp from PyPI
pip install -e . pytest
# Fail instead of silently compiling if a supported Linux wheel is
# missing for one of the advertised Python versions.
python -m pip install --only-binary=najaeda -e . pytest

- name: Run tests
# The CVA6 regressions self-skip without a local snapshot (absent on
# CI); the fast structural + intent + snapshot suite runs.
run: python -m pytest -q

- name: Run the bundled example scripts
# Smoke-tests the exact commands examples/README.md documents (not
# just the answers pytest pins) -- these are small and self-contained
# (no external clone), so they run on every OS/Python combo. The CVA6
# demo has its own external-clone dependency and runs in a separate
# workflow (cva6-demo.yml), not here.
- name: Run bundled example scripts
run: |
python examples/walkthrough.py
python examples/gate_level.py

platforms:
name: ${{ matrix.name }} (Python 3.13)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux x86_64
os: ubuntu-24.04
expected_architecture: x86_64
najaeda_source_build: false
- name: Linux aarch64
os: ubuntu-24.04-arm
expected_architecture: aarch64
najaeda_source_build: false
- name: macOS arm64
os: macos-15
expected_architecture: aarch64
najaeda_source_build: false
- name: macOS x86_64
os: macos-15-intel
expected_architecture: x86_64
# najaeda does not currently publish macOS x86_64 wheels. Building
# its sdist here verifies that the native fallback remains usable.
najaeda_source_build: true
- name: Windows x86_64
os: windows-2025
expected_architecture: x86_64
najaeda_source_build: false
# Windows arm64 is not listed because najaeda does not currently ship
# a win_arm64 wheel or support that source-build path.
steps:
- uses: actions/checkout@v7

- name: Set up Python 3.13
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Verify runner architecture
run: python ci/check_architecture.py "${{ matrix.expected_architecture }}"

- name: Install najaeda source-build dependencies
if: matrix.najaeda_source_build
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_CLEANUP: "1"
run: brew install cmake boost capnp tbb

- name: Install najaeda from source
if: matrix.najaeda_source_build
env:
CMAKE_BUILD_PARALLEL_LEVEL: "4"
run: |
python -m pip install --upgrade pip
python -m pip install --no-binary=najaeda "najaeda>=0.7.25"

- name: Install package + test dependencies from wheels
if: ${{ !matrix.najaeda_source_build }}
run: |
python -m pip install --upgrade pip
# These are the platforms for which najaeda promises binary wheels;
# fail clearly if the expected native artifact is absent.
python -m pip install --only-binary=najaeda -e . pytest

- name: Install package + test dependencies after source build
if: matrix.najaeda_source_build
run: python -m pip install -e . pytest

- name: Run tests
run: python -m pytest -q

- name: Run bundled example scripts
run: |
python examples/walkthrough.py
python examples/gate_level.py
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,31 @@ types, hierarchy, etc.), use `naja.NLUniverse` / `naja.NLDB` /
`naja.SNL*` objects directly, the same way `loader.py`, `session.py`, and
`api.py` do. Do not import or call `najaeda.netlist.*` even for quick
throwaway scripts.

## Regularly recheck the latest CVA6 revision

When upgrading najaeda, preparing a naja-scope release, or maintaining the
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-09-26, the verified local regression baseline is CVA6 v5.3.0
(`2ef1c1b1fca419354920c5487293bc605294904e`), configuration
`cv32a6_imac_sv32`, with najaeda 0.7.25. 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`.

The newer CVA6 checkout at `d40b9540` failed with both najaeda 0.7.24 and
0.7.25 in `core/cva6_mmu/cva6_mmu.sv:375`: "unable to resolve always_comb
condition bit for kind#0". An upstream CVA6 issue has been opened; check its
status and retry rather than assuming the limitation remains.

Test candidate revisions in an isolated checkout with their pinned
submodules, preserving the user's working checkout. Elaborate through the
raw naja API, rebuild and reload the snapshot, and run all three regressions
without weakening their assertions. Before changing the shared demo pin,
also validate its default `cv64a6_imafdc_sv39` configuration. Promote a newer
revision only after these checks pass; record its exact commit and najaeda
version. Otherwise retain the working baseline and record the remaining
failure and date of the check.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ gets back small, exact answers with file-and-line references.

Built on the [najaeda](https://github.com/najaeda/naja) netlist engine.

**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,
and supported constructs may change. `get_intent`/`load_intent` remain
SystemVerilog-only; VHDL source ranges are not guaranteed.


---

## Why
Expand Down Expand Up @@ -225,6 +234,11 @@ build required. The CVA6 cross-hierarchy cone regression
(`tests/test_zzz_cone_cva6.py`) is slow and skips automatically unless a CVA6
snapshot is present.

CI tests every supported Python version on Linux x86_64, plus native platform
lanes for Linux x86_64/aarch64, macOS x86_64/arm64, and Windows x86_64. The
macOS x86_64 lane builds `najaeda` from its source distribution because PyPI
does not currently provide an Intel macOS wheel.

---

## Support & contact
Expand Down
9 changes: 9 additions & 0 deletions README_PyPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ gets back small, exact answers with file-and-line references.

Built on the [najaeda](https://github.com/najaeda/naja) netlist engine.

**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,
and supported constructs may change. `get_intent`/`load_intent` remain
SystemVerilog-only; VHDL source ranges are not guaranteed.


---

## Why
Expand Down
47 changes: 47 additions & 0 deletions ci/check_architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Fail CI when a runner label resolves to an unexpected CPU architecture."""

from __future__ import annotations

import platform
import sys


ALIASES = {
"amd64": "x86_64",
"x64": "x86_64",
"x86_64": "x86_64",
"aarch64": "aarch64",
"arm64": "aarch64",
}


def canonical_architecture(value: str) -> str:
"""Normalize the architecture names used by Python and GitHub runners."""
normalized = value.strip().lower()
return ALIASES.get(normalized, normalized)


def main() -> int:
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <expected-architecture>", file=sys.stderr)
return 2

expected = canonical_architecture(sys.argv[1])
reported = platform.machine()
actual = canonical_architecture(reported)
if actual != expected:
print(
f"architecture mismatch: expected {expected}, "
f"platform.machine() reported {reported!r} ({actual})",
file=sys.stderr,
)
return 1

print(f"verified native architecture: {reported} ({actual})")
return 0


if __name__ == "__main__":
raise SystemExit(main())
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ build-backend = "setuptools.build_meta"
[project]
name = "naja-scope"
dynamic = ["version"]
description = "Agent-facing MCP query layer over najaeda: navigate elaborated SystemVerilog designs without loading source into context."
description = "Agent-facing MCP query layer over najaeda: navigate elaborated SystemVerilog and VHDL designs without loading source into context."
readme = "README_PyPI.md"
license = "Apache-2.0"
license-files = ["LICENSE"]
requires-python = ">=3.10"
authors = [
{ name = "Kepler Technologies", email = "contact@keplertech.io" },
]
keywords = ["mcp", "systemverilog", "eda", "netlist", "rtl", "najaeda", "llm", "agent"]
keywords = ["mcp", "systemverilog", "vhdl", "eda", "netlist", "rtl", "najaeda", "llm", "agent"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -29,7 +30,7 @@ classifiers = [
"Topic :: Software Development :: Libraries",
]
dependencies = [
"najaeda>=0.7.24",
"najaeda>=0.7.25",
"mcp>=1.28,<2",
]

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.15"
__version__ = "0.1.16"
17 changes: 15 additions & 2 deletions src/naja_scope/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ def status() -> dict:
# Intent layer (get_intent): warm-only, so report whether it is
# live in this session and whether the inputs to (re)load it are known.
"intent_loaded": SESSION.intent_available,
"intent_loadable": bool((SESSION.load_spec or {}).get("flist")
or (SESSION.load_spec or {}).get("files")),
"intent_loadable": (SESSION.load_spec.get("language") != "vhdl"
and bool(SESSION.load_spec.get("flist")
or SESSION.load_spec.get("files"))),
}
return out

Expand Down Expand Up @@ -137,6 +138,18 @@ def load_verilog(files: List[str], keep_assigns: bool = True,
return {"top": _summary(top_instance)}


def load_vhdl(file: str, top: Optional[str] = None) -> dict:
"""Load a single VHDL file using 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}
if node is None:
out["note"] = ("Source retained without elaborating a top; package-only "
"files and entities with required generics can do this. "
"Load the dependent top file next.")
return out


def load_liberty(files: List[str]) -> dict:
loader.load_liberty(files)
return {"ok": True}
Expand Down
2 changes: 1 addition & 1 deletion src/naja_scope/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def to_dict(self) -> dict:
class NoDesignError(ScopeError):
def __init__(self):
super().__init__(
"No design loaded. Call load_systemverilog / load_verilog / "
"No design loaded. Call load_systemverilog / load_verilog / load_vhdl / "
"load_snapshot first."
)

Expand Down
23 changes: 21 additions & 2 deletions src/naja_scope/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@

from najaeda import naja

from .errors import SVInternalError, SVSyntaxError, SVUnsupportedError
from .errors import ScopeError, SVInternalError, SVSyntaxError, SVUnsupportedError

# najaeda>=0.7.9 introduced these typed exceptions; naja-scope currently
# requires najaeda>=0.7.24.
# requires najaeda>=0.7.25.
# 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 Expand Up @@ -138,6 +138,25 @@ def load_verilog(files: List[str], keep_assigns: bool = True,
)


def load_vhdl(file: str, top: Optional[str] = None):
"""Load one VHDL source 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.
"""
if not file or not os.path.isfile(file):
raise ScopeError(f"VHDL source file not found: {file}")
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)
except RuntimeError as exc:
raise ScopeError(f"VHDL beta loading failed: {exc}") from exc


def load_liberty(files: List[str]):
if not files:
raise Exception("No liberty files provided")
Expand Down
Loading
Loading