From 096ec455f05d1d7a527ace37cbb60d0317061b2f Mon Sep 17 00:00:00 2001 From: xtof Date: Fri, 25 Sep 2026 17:59:30 +0200 Subject: [PATCH 01/11] bump to 0.7.26 --- src/core/NajaVersion.h.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/NajaVersion.h.in b/src/core/NajaVersion.h.in index 429a10ab8..0050d0947 100644 --- a/src/core/NajaVersion.h.in +++ b/src/core/NajaVersion.h.in @@ -26,7 +26,7 @@ inline std::string getVersionPart(const std::string& version, int index) { } // namespace detail -const std::string NAJA_VERSION { "0.7.25" }; +const std::string NAJA_VERSION { "0.7.26" }; const std::string NAJA_MAJOR { detail::getVersionPart(NAJA_VERSION, 0) }; const std::string NAJA_MINOR { detail::getVersionPart(NAJA_VERSION, 1) }; const std::string NAJA_PATCH { detail::getVersionPart(NAJA_VERSION, 2) }; From a1f6d1ed0b61110281031b1c6d6e7b81ea2bfe13 Mon Sep 17 00:00:00 2001 From: xtof Date: Fri, 25 Sep 2026 18:17:11 +0200 Subject: [PATCH 02/11] accelerate windows wheels --- .github/workflows/wheels.yml | 66 +++++++++++++++++++++++------------- ci/windows_setup.ps1 | 33 ++++++++++++------ 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 10823d8ef..49152d676 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -25,7 +25,7 @@ jobs: outputs: artifact_name: parser-generated-sources steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: submodules: true @@ -44,7 +44,7 @@ jobs: cp -v /usr/include/FlexLexer.h ../../generated - name: Upload parser artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: parser-generated-sources path: generated/* @@ -162,16 +162,16 @@ jobs: steps: # build steps - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: submodules: true - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7.0.0 with: python-version: "3.11" - name: Download parser-generated sources # if: runner.os == 'Windows' - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v8.0.1 with: name: parser-generated-sources path: generated @@ -189,6 +189,33 @@ jobs: brew install -q boost - name: Install cibuildwheel (and delvewheel for Windows) run: python -m pip install cibuildwheel==4.2.1 delvewheel + - name: Configure Windows dependency cache + if: runner.os == 'Windows' + shell: pwsh + run: | + $cachePath = Join-Path $env:RUNNER_TEMP 'vcpkg-binary-cache' + New-Item -ItemType Directory -Force -Path $cachePath | Out-Null + Add-Content $env:GITHUB_ENV "VCPKG_BINARY_SOURCES=clear;files,$cachePath,readwrite" + Add-Content $env:GITHUB_ENV "VCPKG_CACHE_IMAGE=$env:ImageOS-$env:ImageVersion" + - name: Restore Windows dependency cache + if: runner.os == 'Windows' + id: vcpkg-cache + uses: actions/cache/restore@v6.1.0 + with: + path: ${{ runner.temp }}/vcpkg-binary-cache + # Share native dependencies across Python versions; refresh on runner + # image updates so a new compiler can populate its own cache. + key: vcpkg-${{ runner.os }}-${{ runner.arch }}-${{ env.VCPKG_CACHE_IMAGE }}-${{ hashFiles('ci/windows_setup.ps1') }} + - name: Install Windows dependencies + if: runner.os == 'Windows' + shell: pwsh + run: ./ci/windows_setup.ps1 + - name: Save Windows dependency cache + if: runner.os == 'Windows' && steps.vcpkg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6.1.0 + with: + path: ${{ runner.temp }}/vcpkg-binary-cache + key: ${{ steps.vcpkg-cache.outputs.cache-primary-key }} - name: Build wheels env: CIBW_ENVIRONMENT: SKLEARN_SKIP_NETWORK_TESTS=1 @@ -202,9 +229,6 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }} CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux_image }} CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_image }} - CIBW_BEFORE_ALL_WINDOWS: > - pwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass - -File {project}\ci\windows_setup.ps1 CIBW_BEFORE_BUILD_WINDOWS: "pwsh -Command \"foreach($d in 'build','dist'){ if(Test-Path $d){ Remove-Item -Recurse -Force $d -ErrorAction SilentlyContinue } }\"" CIBW_CONFIG_SETTINGS_WINDOWS: > cmake.args=-DCMAKE_TOOLCHAIN_FILE=C:/Users/runneradmin/vcpkg/scripts/buildsystems/vcpkg.cmake @@ -274,9 +298,7 @@ jobs: echo "macOS wheel compatibility check failed." exit 1 fi - #Reminder!!: actions/upload-artifact and actions/download-artifact - #need to have same version - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: cibw-wheels-cp${{ matrix.python }}-${{ matrix.platform_id }} path: ./wheelhouse/*.whl @@ -286,12 +308,12 @@ jobs: name: Build source distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: submodules: true - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7.0.0 with: python-version: "3.11" @@ -301,7 +323,7 @@ jobs: sudo apt-get install -yq libboost-dev capnproto libcapnp-dev libtbb-dev ninja-build - name: Download parser-generated sources - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v8.0.1 with: name: parser-generated-sources path: generated @@ -324,7 +346,7 @@ jobs: python -m pip install --force-reinstall sdist-wheel/*.whl python ci/check_wheel_import.py - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: sdist path: dist/*.tar.gz @@ -338,20 +360,18 @@ jobs: permissions: id-token: write steps: - #Reminder!!: actions/upload-artifact and actions/download-artifact - #need to have same version - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: pattern: cibw-* path: dist merge-multiple: true - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: name: sdist path: dist - - uses: pypa/gh-action-pypi-publish@v1.14.0 + - uses: pypa/gh-action-pypi-publish@v1.14.2 with: password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository-url: https://test.pypi.org/legacy/ @@ -365,16 +385,16 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: pattern: cibw-* path: dist merge-multiple: true - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: name: sdist path: dist - - uses: pypa/gh-action-pypi-publish@v1.14.0 + - uses: pypa/gh-action-pypi-publish@v1.14.2 with: password: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true diff --git a/ci/windows_setup.ps1 b/ci/windows_setup.ps1 index df98af039..fdcbd65b9 100644 --- a/ci/windows_setup.ps1 +++ b/ci/windows_setup.ps1 @@ -2,19 +2,30 @@ # # SPDX-License-Identifier: Apache-2.0 -Write-Host "Installing vcpkg (latest)" +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true -git clone https://github.com/microsoft/vcpkg.git $env:USERPROFILE/vcpkg -& "$env:USERPROFILE/vcpkg/bootstrap-vcpkg.bat" +# The wheel cache key includes this script, so dependency changes refresh it. +$vcpkgRevision = '10541e317a660f4165ba4ac2851ab54a8d4577b1' +$vcpkgRoot = "$env:USERPROFILE/vcpkg" +Write-Host "Installing vcpkg ($vcpkgRevision)" -# Install dependencies -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install capnproto:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install tbb:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install boost-intrusive:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install boost-dynamic-bitset:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install boost-multiprecision:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install boost-unordered:x64-windows -& "$env:USERPROFILE/vcpkg/vcpkg.exe" install boost-regex:x64-windows +git init $vcpkgRoot +git -C $vcpkgRoot fetch --depth 1 https://github.com/microsoft/vcpkg.git $vcpkgRevision +git -C $vcpkgRoot checkout --detach FETCH_HEAD +& "$vcpkgRoot/bootstrap-vcpkg.bat" -disableMetrics + +# vcpkg validates each binary's ABI before reusing it from the restored cache. +$packages = @( + 'capnproto:x64-windows' + 'tbb:x64-windows' + 'boost-intrusive:x64-windows' + 'boost-dynamic-bitset:x64-windows' + 'boost-multiprecision:x64-windows' + 'boost-unordered:x64-windows' + 'boost-regex:x64-windows' +) +& "$vcpkgRoot/vcpkg.exe" install @packages # Export for CMake Add-Content $env:GITHUB_ENV "CMAKE_TOOLCHAIN_FILE=$env:USERPROFILE/vcpkg/scripts/buildsystems/vcpkg.cmake" From 05b9a90f7f337bad1f8157f5343fe57942e67edb Mon Sep 17 00:00:00 2001 From: xtof Date: Sat, 26 Sep 2026 12:50:41 +0200 Subject: [PATCH 03/11] better diagnostic for slang invalid expression --- .../frontend/SNLSVConstructor.cpp | 19 +++++++++++++- .../frontend/SNLSVConstructorTestSimple.cpp | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp index a1239bb7d..eec818bd8 100644 --- a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp +++ b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp @@ -18214,6 +18214,9 @@ endmodule // LCOV_EXCL_START std::string describeExpressionKind(slang::ast::ExpressionKind kind) const { + if (kind == slang::ast::ExpressionKind::Invalid) { + return "InvalidExpression"; + } if (kind == slang::ast::ExpressionKind::UnaryOp) { return "UnaryOp"; } @@ -18315,7 +18318,21 @@ endmodule } std::ostringstream description; description << describeExpressionKind(stripped->kind); - if (stripped->kind == slang::ast::ExpressionKind::BinaryOp) { + if (stripped->kind == slang::ast::ExpressionKind::Invalid) { + description << " (Slang rejected the expression; check Slang diagnostics)"; + // InvalidExpression has no source range; its retained child can still + // identify the expression that failed semantic checking. + const auto* child = stripped->as().child; + if (child) { + const auto range = getSourceRange(*child); + if (const auto info = getSourceInfo(range)) { + description << " at " << info->file << ":" << info->line << ":" << info->column; + } + if (const auto excerpt = getSourceExcerpt(range)) { + description << " [" << *excerpt << "]"; + } + } + } else if (stripped->kind == slang::ast::ExpressionKind::BinaryOp) { const auto& binaryExpr = stripped->as(); description << " op=" << slang::ast::OpInfo::getText(binaryExpr.op); } else if (stripped->kind == slang::ast::ExpressionKind::UnaryOp) { diff --git a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp index d538fa53f..63aa4f572 100644 --- a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp +++ b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp @@ -9596,6 +9596,32 @@ TEST_F(SNLSVConstructorTestSimple, parseNonAnsiUnnamedMultiPortReportedUnsupport } } +TEST_F(SNLSVConstructorTestSimple, parseInvalidGuardedRangeConditionDiagnostic) { + SNLSVConstructor constructor(library_); + const auto outPath = std::filesystem::path(SNL_SV_DUMPER_TEST_PATH) / + "invalid_guarded_range_condition"; + std::filesystem::create_directories(outPath); + const auto svPath = outPath / "repro.sv"; + std::ofstream svFile(svPath); + ASSERT_TRUE(svFile.good()); + svFile << "module repro(input logic en, input logic [31:0] addr, output logic fault);\n" + << " localparam bit RV64 = 0;\n" + << " always_comb begin\n" + << " fault = 0;\n" + << " if (en && RV64 && (|addr[31:34] != 1'b0)) fault = 1;\n" + << " end\n" + << "endmodule\n"; + svFile.close(); + + // Slang diagnoses the reversed range as a warning but invalidates the + // enclosing condition before Naja can fold its constant-false guard. + expectUnsupportedConstruct( + constructor, + svPath, + {"InvalidExpression", "Slang rejected the expression", "repro.sv:5:", + "en && RV64 && (|addr[31:34] != 1'b0)"}); +} + TEST_F(SNLSVConstructorTestSimple, parseUnsupportedElementsReportedAtEnd) { SNLSVConstructor constructor(library_); std::filesystem::path outPath(SNL_SV_DUMPER_TEST_PATH); From b79de03754ad7c031c89f213d291d1a996a63dbc Mon Sep 17 00:00:00 2001 From: xtof Date: Sat, 26 Sep 2026 12:54:29 +0200 Subject: [PATCH 04/11] clean versions --- .github/workflows/wheels.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 49152d676..60d96c22c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -212,7 +212,7 @@ jobs: run: ./ci/windows_setup.ps1 - name: Save Windows dependency cache if: runner.os == 'Windows' && steps.vcpkg-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v6.1.0 + uses: actions/cache/save@v6 with: path: ${{ runner.temp }}/vcpkg-binary-cache key: ${{ steps.vcpkg-cache.outputs.cache-primary-key }} @@ -298,7 +298,7 @@ jobs: echo "macOS wheel compatibility check failed." exit 1 fi - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@v7 with: name: cibw-wheels-cp${{ matrix.python }}-${{ matrix.platform_id }} path: ./wheelhouse/*.whl @@ -308,12 +308,12 @@ jobs: name: Build source distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@v7 with: submodules: true - name: Setup Python - uses: actions/setup-python@v7.0.0 + uses: actions/setup-python@v7 with: python-version: "3.11" @@ -323,7 +323,7 @@ jobs: sudo apt-get install -yq libboost-dev capnproto libcapnp-dev libtbb-dev ninja-build - name: Download parser-generated sources - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@v8 with: name: parser-generated-sources path: generated @@ -346,7 +346,7 @@ jobs: python -m pip install --force-reinstall sdist-wheel/*.whl python ci/check_wheel_import.py - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@v7 with: name: sdist path: dist/*.tar.gz @@ -360,13 +360,13 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@v8 with: pattern: cibw-* path: dist merge-multiple: true - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@v8 with: name: sdist path: dist @@ -385,12 +385,12 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@v8 with: pattern: cibw-* path: dist merge-multiple: true - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@v8 with: name: sdist path: dist From 131452a77bb16847778f26957f6cd7a391757f05 Mon Sep 17 00:00:00 2001 From: xtof Date: Sat, 26 Sep 2026 17:20:04 +0200 Subject: [PATCH 05/11] update colabs --- CLAUDE.md | 1 + tutorials/README.md | 2 +- tutorials/notebooks/01_getting_started.ipynb | 2 +- tutorials/notebooks/02_liberty_primitives_design.ipynb | 2 +- tutorials/notebooks/03_editing_a_netlist.ipynb | 2 +- tutorials/notebooks/04_systemverilog_elaborated_netlist.ipynb | 2 +- tutorials/notebooks/05_ibex_riscv_core.ipynb | 2 +- tutorials/notebooks/06_fanout_analysis.ipynb | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3df71fae..136fce38d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,6 +119,7 @@ ci/check_submodule_bazel_sync.py`. ## Conventions +- Whenever the `najaeda` version is incremented, update the pinned `najaeda` version in all Colab tutorials under `tutorials/notebooks/` and the local installation command in `tutorials/README.md` in the same change. - Every new file, including test fixtures and helper scripts, must have both copyright and licensing information. For Naja-authored files, add `SPDX-FileCopyrightText: The Naja authors ` and `SPDX-License-Identifier: Apache-2.0` using the file format's comment syntax. Preserve third-party attribution and licenses. For files that cannot contain comments, add coverage in `.reuse/dep5`. Before finishing a change that adds files or changes licensing metadata, run `reuse lint` and fix any missing copyright or licensing information introduced by the change. - Match the surrounding code's style, naming, and comment density — the SNL layer uses `NL*`/`SNL*` prefixes; follow the local idiom. - The SystemVerilog frontend is built on **slang**; sequential lowering and always-block handling live in `SNLSVConstructor` and the "Sequential Assignment Lowering" community — query the graph before touching them. diff --git a/tutorials/README.md b/tutorials/README.md index 74349a149..ef38a12f4 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -23,7 +23,7 @@ Design files used by the tutorials are in [`benchmarks/`](benchmarks/): ## Running Locally ```bash -pip install najaeda==0.7.6 jupyter nbmake nbstripout +pip install najaeda==0.7.25 jupyter nbmake nbstripout # one-time git hook to keep notebooks clean nbstripout --install # run all tutorials as a test suite diff --git a/tutorials/notebooks/01_getting_started.ipynb b/tutorials/notebooks/01_getting_started.ipynb index 50d3d9c70..6f00d10de 100644 --- a/tutorials/notebooks/01_getting_started.ipynb +++ b/tutorials/notebooks/01_getting_started.ipynb @@ -43,7 +43,7 @@ }, "outputs": [], "source": [ - "!pip install najaeda==0.7.6\n" + "!pip install najaeda==0.7.25\n" ] }, { diff --git a/tutorials/notebooks/02_liberty_primitives_design.ipynb b/tutorials/notebooks/02_liberty_primitives_design.ipynb index a54a63acc..9a7d101fc 100644 --- a/tutorials/notebooks/02_liberty_primitives_design.ipynb +++ b/tutorials/notebooks/02_liberty_primitives_design.ipynb @@ -40,7 +40,7 @@ "!wget https://github.com/najaeda/naja/archive/refs/heads/main.zip\n", "!unzip main.zip\n", "os.environ['NAJAEDA_TUTORIALS_BENCHMARKS'] = 'naja-main/tutorials/benchmarks'\n", - "!pip install najaeda==0.7.6" + "!pip install najaeda==0.7.25" ] }, { diff --git a/tutorials/notebooks/03_editing_a_netlist.ipynb b/tutorials/notebooks/03_editing_a_netlist.ipynb index 1f338dc4f..f06d40247 100644 --- a/tutorials/notebooks/03_editing_a_netlist.ipynb +++ b/tutorials/notebooks/03_editing_a_netlist.ipynb @@ -38,7 +38,7 @@ }, "outputs": [], "source": [ - "!pip install najaeda==0.7.6" + "!pip install najaeda==0.7.25" ] }, { diff --git a/tutorials/notebooks/04_systemverilog_elaborated_netlist.ipynb b/tutorials/notebooks/04_systemverilog_elaborated_netlist.ipynb index 8eece3489..13e4c5149 100644 --- a/tutorials/notebooks/04_systemverilog_elaborated_netlist.ipynb +++ b/tutorials/notebooks/04_systemverilog_elaborated_netlist.ipynb @@ -32,7 +32,7 @@ }, "outputs": [], "source": [ - "!pip install najaeda==0.7.6" + "!pip install najaeda==0.7.25" ] }, { diff --git a/tutorials/notebooks/05_ibex_riscv_core.ipynb b/tutorials/notebooks/05_ibex_riscv_core.ipynb index a29cc2634..d4914a0d4 100644 --- a/tutorials/notebooks/05_ibex_riscv_core.ipynb +++ b/tutorials/notebooks/05_ibex_riscv_core.ipynb @@ -38,7 +38,7 @@ "import os\n", "from pathlib import Path\n", "\n", - "!pip install najaeda==0.7.6 'fusesoc>=2.4'\n", + "!pip install najaeda==0.7.25 'fusesoc>=2.4'\n", "\n", "# Clone ibex at the pinned commit known to work with najaeda\n", "!git clone https://github.com/lowRISC/ibex.git ibex\n", diff --git a/tutorials/notebooks/06_fanout_analysis.ipynb b/tutorials/notebooks/06_fanout_analysis.ipynb index 0e8ae2f36..bbb134dd1 100644 --- a/tutorials/notebooks/06_fanout_analysis.ipynb +++ b/tutorials/notebooks/06_fanout_analysis.ipynb @@ -49,7 +49,7 @@ "!wget https://github.com/najaeda/naja/archive/refs/heads/main.zip\n", "!unzip main.zip\n", "os.environ['NAJAEDA_TUTORIALS_BENCHMARKS'] = 'naja-main/tutorials/benchmarks'\n", - "!pip install najaeda==0.7.6 pandas matplotlib" + "!pip install najaeda==0.7.25 pandas matplotlib" ] }, { From 7a921ac4b278193fe598e69e8178df11f1170237 Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 27 Sep 2026 00:10:06 +0200 Subject: [PATCH 06/11] VHDL table select --- src/najaeda/najaeda/docs/source/loading.rst | 11 ++ .../formats/verilog/backend/SNLVRLDumper.cpp | 12 ++ src/nl/formats/vhdl/VHDLRTLConstructor.cpp | 126 ++++++++++++++---- src/vhdl/README.md | 12 ++ .../backend/SNLVRLDumperTestParameters.cpp | 20 +++ test/nl/formats/vhdl/CMakeLists.txt | 13 +- test/nl/formats/vhdl/VHDLConstructorTest.cpp | 108 +++++++++++++++ test/nl/formats/vhdl/VHDLTestUtils.h | 10 +- .../nl/formats/vhdl/compare_indexed_tables.py | 93 +++++++++++++ test/nl/formats/vhdl/indexed_tables.vhd | 42 ++++++ 10 files changed, 421 insertions(+), 26 deletions(-) create mode 100644 test/nl/formats/vhdl/compare_indexed_tables.py create mode 100644 test/nl/formats/vhdl/indexed_tables.vhd diff --git a/src/najaeda/najaeda/docs/source/loading.rst b/src/najaeda/najaeda/docs/source/loading.rst index 5994d75fd..d7df73811 100644 --- a/src/najaeda/najaeda/docs/source/loading.rst +++ b/src/najaeda/najaeda/docs/source/loading.rst @@ -222,6 +222,17 @@ at ``neorv32_top.vhd:30`` in the development checkout. Vector generics, runtime vector helpers, and further elaboration constructs remain unsupported; the complete top has not been validated. +Dynamic array reads lower to ``naja_table_select`` for constant arrays, +constant-driven ROM signals, and general signal or variable arrays. Identical +lookups within a design share the primitive. Nonnegative ascending and descending +bounds are supported, including nonzero lower bounds. Indices can use +``conv_integer`` with the Synopsys packages or ``to_integer(unsigned(...))`` +with ``numeric_std`` (up to 31 input bits). Address bits are preserved so +out-of-range or unknown addresses produce X in exported Verilog rather than +wrapping to a valid entry. Dynamic negative bounds and signed ``to_integer`` +remain unsupported. Inferred writable memories retain their existing memory +primitive lowering. + The RTL path supports ``numeric_std.unsigned`` and ``numeric_std.signed`` addition, subtraction, multiplication, equality/inequality and concatenation. Static ``to_unsigned(value, size)`` calls and conversions between ``unsigned``, ``signed``, diff --git a/src/nl/formats/verilog/backend/SNLVRLDumper.cpp b/src/nl/formats/verilog/backend/SNLVRLDumper.cpp index 39adfb923..4f328c0ef 100644 --- a/src/nl/formats/verilog/backend/SNLVRLDumper.cpp +++ b/src/nl/formats/verilog/backend/SNLVRLDumper.cpp @@ -2131,6 +2131,18 @@ void SNLVRLDumper::dumpTermAssigns(const SNLDesign* design, std::ostream& o) { for (auto term: design->getBitTerms()) { auto net = term->getNet(); if (net) { + if (term->getDirection() == SNLTerm::Direction::Output && + (net->isAssign0() || net->isAssign1() || net->isAssignX() || net->isAssignZ())) { + // Constants substitute at input uses, but must also drive top-level + // outputs, including bits whose net and terminal share the same name. + auto* busBit = dynamic_cast(term); + const auto name = busBit ? + dumpName(busBit->getBus()->getName().getString()) + "[" + std::to_string(busBit->getBit()) + "]" : + dumpName(term->getName().getString()); + o << "assign " << name << " = 1'b" << getAssignConstantBitValue(net) << ";\n"; + atLeastOne = true; + continue; + } if (auto scalarTerm = dynamic_cast(term)) { if (auto scalarNet = dynamic_cast(net)) { //same name ? diff --git a/src/nl/formats/vhdl/VHDLRTLConstructor.cpp b/src/nl/formats/vhdl/VHDLRTLConstructor.cpp index a87fcc70f..0c95b3a99 100644 --- a/src/nl/formats/vhdl/VHDLRTLConstructor.cpp +++ b/src/nl/formats/vhdl/VHDLRTLConstructor.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include namespace naja::NL { @@ -1814,6 +1815,8 @@ class RTLConstructor { } Bits mux(SNLBitNet* condition, const Bits& yes, const Bits& no) { if (auto value = constantValue(condition)) return *value ? yes : no; + const auto cacheKey = std::make_tuple(condition, yes, no); + if (const auto found = muxes_.find(cacheKey); found != muxes_.end()) return found->second; Bits result(yes.size()); for (size_t first = 0; first < yes.size();) { if (yes[first] == no[first] || !yes[first] || !no[first]) { @@ -1837,8 +1840,24 @@ class RTLConstructor { : static_cast(output)->getBitAtPosition(i - first); first = end; } + muxes_.emplace(cacheKey, result); return result; } + SNLBitNet* cachedGate(const std::string& op, Bits inputs) { + // All supported multi-input gates are commutative. + std::sort(inputs.begin(), inputs.end(), std::less{}); + const auto cacheKey = std::make_pair(op, inputs); + if (const auto found = gates_.find(cacheKey); found != gates_.end()) return found->second; + using Gate = SNLRTLPrimitives::GateKind; + static const std::map kinds{{"and", Gate::And}, {"or", Gate::Or}, + {"xor", Gate::Xor}, {"nand", Gate::Nand}, {"nor", Gate::Nor}, + {"xnor", Gate::Xnor}, {"not", Gate::Not}}; + auto* output = SNLScalarNet::create(design_); + SNLRTLPrimitives::createGate(design_, kinds.at(op), + std::vector(inputs.begin(), inputs.end()), output); + gates_.emplace(cacheKey, output); + return output; + } SNLBitNet* gate(const std::string& op, SNLBitNet* a, SNLBitNet* b = nullptr) { using Gate = SNLRTLPrimitives::GateKind; static const std::map kinds{{"and", Gate::And}, {"or", Gate::Or}, @@ -1853,11 +1872,20 @@ class RTLConstructor { if (op == "nand" || op == "nor" || op == "xnor") value = !value; return constant(value); } - auto* output = SNLScalarNet::create(design_); - std::vector inputs{a}; - if (b) inputs.push_back(b); - SNLRTLPrimitives::createGate(design_, kind->second, inputs, output); - return output; + if (b && (av || bv)) { + const bool value = av ? *av : *bv; + auto* other = av ? b : a; + if (op == "and" || op == "nand") { + auto* result = value ? other : constant(false); + return op == "nand" ? gate("not", result) : result; + } + if (op == "or" || op == "nor") { + auto* result = value ? constant(true) : other; + return op == "nor" ? gate("not", result) : result; + } + return (value == (op == "xor")) ? gate("not", other) : other; + } + return cachedGate(op, b ? Bits{a, b} : Bits{a}); } Bits add(const Bits& left, const Bits& right, bool subtract) { @@ -1912,10 +1940,16 @@ class RTLConstructor { } SNLBitNet* equal(const Bits& a, const Bits& b) { if (a.size() != b.size()) fail("internal comparison width mismatch"); - auto* result = constant(true); - for (size_t i = 0; i < a.size(); ++i) - result = gate("and", result, gate("xnor", a[i], b[i])); - return result; + Bits matches; + for (size_t i = 0; i < a.size(); ++i) { + auto* match = gate("xnor", a[i], b[i]); + if (const auto value = constantValue(match)) { + if (!*value) return constant(false); + } else matches.push_back(match); + } + if (matches.empty()) return constant(true); + if (matches.size() == 1) return matches.front(); + return cachedGate("and", std::move(matches)); } Shape vectorShape(size_t width, const std::string& scalar = "std_logic") { if (!width) fail("empty vector"); @@ -1970,16 +2004,54 @@ class RTLConstructor { for (auto* bit : value.bits) readBit(bit); auto address = expression(indexExpr, state); checkIndexType(address, bounds); - Bits result(stride, constant(false)); - for (size_t i = 0; i < bounds.size(); ++i) { - auto index = bounds.ascending ? bounds.left + int64_t(i) : bounds.left - int64_t(i); - if (index < 0) fail("dynamic indexing of negative bounds is unsupported"); - const auto width = std::max(address.bits.size(), 32); - auto* select = equal(resize(address.bits, width), number(index, {{"integer"}, {}, width}).bits); - result = mux(select, Bits(value.bits.begin() + i * stride, - value.bits.begin() + (i + 1) * stride), result); - } - value.bits = std::move(result); + const auto low = std::min(bounds.left, bounds.right); + const auto high = std::max(bounds.left, bounds.right); + if (low < 0) fail("dynamic indexing of negative bounds is unsupported"); + // Retain every nonconstant address bit. Truncating to ceil(log2(depth)) + // would alias out-of-range reads onto valid entries. Widen narrow indices + // too, so unreachable entries cannot alias in the primitive model. + while (address.bits.size() > 1 && constantValue(address.bits.front()) == false) + address.bits.erase(address.bits.begin()); + size_t width = 1; + for (auto bound = uint64_t(high); bound >>= 1;) ++width; + width = std::max(width, address.bits.size()); + address.bits = resize(address.bits, width); + if (low) address.bits = add(address.bits, number(low, {{"integer"}, {}, width}).bits, true); + auto* model = NLDB0::getOrCreateTableSelect({stride, bounds.size(), width}); + Bits data; + for (size_t entry = 0; entry < bounds.size(); ++entry) { + const auto position = bounds.ascending ? entry : bounds.size() - 1 - entry; + for (size_t bit = stride; bit; --bit) { + auto* net = value.bits[position * stride + bit - 1]; + if (const auto binary = constantValue(net)) net = constant(*binary); + data.push_back(net); + } + } + Bits inputs = data; + inputs.insert(inputs.end(), address.bits.begin(), address.bits.end()); + const auto cacheKey = std::make_pair(model, inputs); + if (const auto found = tables_.find(cacheKey); found != tables_.end()) { + value.bits = found->second; + return value; + } + auto* instance = SNLInstance::create(design_, model); + // Verilog export uses the shared parameterized module, not the canonical + // model's name or default dimensions. + for (const auto& [parameter, size] : std::vector>{ + {"WIDTH", stride}, {"DEPTH", bounds.size()}, {"ABITS", width}}) + SNLInstParameter::create(instance, model->getParameter(NLName(parameter)), std::to_string(size)); + for (size_t bit = 0; bit < data.size(); ++bit) + instance->getInstTerm(NLDB0::getTableSelectData(model)->getBit(bit))->setNet(data[bit]); + for (size_t bit = 0; bit < width; ++bit) + instance->getInstTerm(NLDB0::getTableSelectAddress(model)->getBit(bit))->setNet(address.bits[width - 1 - bit]); + auto* output = SNLBusNet::create(design_, stride - 1, 0); + value.bits.clear(); + for (size_t bit = stride; bit; --bit) { + auto* net = output->getBit(bit - 1); + instance->getInstTerm(NLDB0::getTableSelectOutput(model)->getBit(bit - 1))->setNet(net); + value.bits.push_back(net); + } + tables_.emplace(cacheKey, value.bits); return value; } @@ -2096,11 +2168,16 @@ class RTLConstructor { if (value.shape.ranges.size() != 1 || (source != "unsigned" && source != "signed" && source != "std_logic_vector")) fail("unsupported vector conversion operand"); value.shape.types.front() = target; - } else if (expr.left->kind == Expr::Kind::Name && key(*expr.left) == "conv_integer") { - if (!unsigned_ || !arith_) fail("conv_integer requires std_logic_arith and std_logic_unsigned"); + } else if (expr.left->kind == Expr::Kind::Name && + (key(*expr.left) == "conv_integer" || key(*expr.left) == "to_integer")) { + const bool numeric = key(*expr.left) == "to_integer"; + if (numeric ? !numeric_ : (!unsigned_ || !arith_)) + fail(numeric ? "to_integer requires numeric_std" : + "conv_integer requires std_logic_arith and std_logic_unsigned"); value = expression(*expr.right, state); - if (value.shape.types.front() != "std_logic_vector" || value.bits.size() > 31) - fail("unsupported conv_integer argument"); + if (value.shape.ranges.size() != 1 || + value.shape.types.front() != (numeric ? "unsigned" : "std_logic_vector") || value.bits.size() > 31) + fail(numeric ? "unsupported to_integer argument" : "unsupported conv_integer argument"); value.shape = {{"integer"}, {}, value.bits.size()}; } else value = readSelected(expr, state); for (auto* bit : value.bits) readBit(bit); @@ -2740,6 +2817,9 @@ class RTLConstructor { std::string objectPrefix_; std::vector> generatedObjects_; std::map memories_; + std::map, SNLBitNet*> gates_; + std::map, Bits> muxes_; + std::map, Bits> tables_; std::set drivers_, reads_; std::map initialValues_; std::set initializedFlops_; diff --git a/src/vhdl/README.md b/src/vhdl/README.md index c62797088..0b7e14a11 100644 --- a/src/vhdl/README.md +++ b/src/vhdl/README.md @@ -58,6 +58,18 @@ definite assignment, array ports, hierarchy, initialization, and asynchronous processes remain unsupported. Elaboration is bounded to 65,536 bits per object and 100,000 expanded statements. +The current adapter also lowers dynamic reads of constant, signal and variable +arrays to `naja_table_select`, with element nets packed in numeric index order. +Constant-driven ROM signals keep constant DATA even when their assignment appears +after the read. Nonnegative nonzero bounds are normalized without dropping high +address bits; ascending and descending declarations preserve element order. +Unknown and out-of-range addresses yield X in exported Verilog. Both Synopsys +`conv_integer` and `numeric_std.to_integer(unsigned(...))` indices are supported +up to 31 bits; signed conversions and dynamic negative bounds are diagnosed. +Combinational gates, muxes and table reads are shared within each design, gates +fold constant operands, and equality uses a reduction of the nonconstant matches. +Inferred memories continue to use `naja_mem`. + The standalone analyzer and scalar scheduler diagnose syntax requiring this profile; the adapter performs its own checked RTL elaboration and discards the design on any failure. The original scalar integration path remains available. diff --git a/test/nl/formats/verilog/backend/SNLVRLDumperTestParameters.cpp b/test/nl/formats/verilog/backend/SNLVRLDumperTestParameters.cpp index 2ba4418dc..0ac268b03 100644 --- a/test/nl/formats/verilog/backend/SNLVRLDumperTestParameters.cpp +++ b/test/nl/formats/verilog/backend/SNLVRLDumperTestParameters.cpp @@ -316,6 +316,26 @@ TEST_F(SNLVRLDumperTestParameters, testDefaultInstanceParametersAreOmitted) { EXPECT_EQ(std::string::npos, dumped.find(".SAME_STR(")); } +TEST_F(SNLVRLDumperTestParameters, testConstantOutputNetsDrivePorts) { + auto* scalar = SNLScalarTerm::create(top_, SNLTerm::Direction::Output, NLName("constant_out")); + auto* scalarNet = SNLScalarNet::create(top_, NLName("constant_out")); + scalarNet->setType(SNLNet::Type::Assign1); + scalar->setNet(scalarNet); + auto* bus = SNLBusTerm::create(top_, SNLTerm::Direction::Output, 3, 0, NLName("constant.bus")); + auto* busNet = SNLBusNet::create(top_, 3, 0, NLName("constant.bus")); + bus->setNet(busNet); + const SNLNet::Type types[]{SNLNet::Type::Assign0, SNLNet::Type::Assign1, + SNLNet::Type::AssignX, SNLNet::Type::AssignZ}; + for (int bit = 0; bit < 4; ++bit) busNet->getBit(bit)->setType(types[bit]); + std::ostringstream out; + SNLVRLDumper().dumpDesign(top_, out); + const auto dumped = out.str(); + EXPECT_NE(dumped.find("assign constant_out = 1'b1;"), std::string::npos); + for (int bit = 0; bit < 4; ++bit) + EXPECT_NE(dumped.find("assign \\constant.bus [" + std::to_string(bit) + "] = 1'b" + "01xz"[bit] + ";"), + std::string::npos); +} + TEST_F(SNLVRLDumperTestParameters, testFullyUnconnectedInstancePortsAreDumped) { ASSERT_TRUE(top_); ASSERT_TRUE(model_); diff --git a/test/nl/formats/vhdl/CMakeLists.txt b/test/nl/formats/vhdl/CMakeLists.txt index fad4e4a4d..f225bb60f 100644 --- a/test/nl/formats/vhdl/CMakeLists.txt +++ b/test/nl/formats/vhdl/CMakeLists.txt @@ -1,7 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors # SPDX-License-Identifier: Apache-2.0 add_executable(snlVHDLConstructorTests VHDLConstructorTest.cpp) target_compile_definitions(snlVHDLConstructorTests PRIVATE + SNL_VHDL_INDEXED_TABLES="${CMAKE_CURRENT_SOURCE_DIR}/indexed_tables.vhd" SNL_VHDL_ASYNCHRONOUS_RESET="${CMAKE_CURRENT_SOURCE_DIR}/asynchronous_reset.vhd" SNL_VHDL_MEMORY="${CMAKE_CURRENT_SOURCE_DIR}/memory.vhd" SNL_VHDL_GENERATED_FIR="${CMAKE_CURRENT_SOURCE_DIR}/fir_generated.vhd" @@ -31,6 +33,15 @@ target_link_libraries(snlVHDLExternalTests naja_snl_vhdl gtest_main) find_program(VHDL_NVC_EXECUTABLE nvc) find_package(Python3 COMPONENTS Interpreter QUIET) +find_program(VHDL_IVERILOG_EXECUTABLE iverilog) +find_program(VHDL_VVP_EXECUTABLE vvp) +if(Python3_Interpreter_FOUND AND VHDL_IVERILOG_EXECUTABLE AND VHDL_VVP_EXECUTABLE) + add_test(NAME VHDLIndexedTablesVerilog + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/compare_indexed_tables.py" + --adapter $ + --iverilog "${VHDL_IVERILOG_EXECUTABLE}" --vvp "${VHDL_VVP_EXECUTABLE}") + set_tests_properties(VHDLIndexedTablesVerilog PROPERTIES TIMEOUT 180) +endif() if(VHDL_NVC_EXECUTABLE AND Python3_Interpreter_FOUND) add_test(NAME VHDLGeneratedFIRReference COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/compare_fir.py" @@ -38,8 +49,6 @@ if(VHDL_NVC_EXECUTABLE AND Python3_Interpreter_FOUND) --source "${CMAKE_CURRENT_SOURCE_DIR}/fir_generated.vhd" --top fir_generated --lanes 4 --width 5 --test VHDLConstructorTest.GeneratedFIRHierarchyCycles) set_tests_properties(VHDLGeneratedFIRReference PROPERTIES TIMEOUT 180) - find_program(VHDL_IVERILOG_EXECUTABLE iverilog) - find_program(VHDL_VVP_EXECUTABLE vvp) set(VHDL_MEMORY_DUMP_ARGS) if(VHDL_IVERILOG_EXECUTABLE AND VHDL_VVP_EXECUTABLE) list(APPEND VHDL_MEMORY_DUMP_ARGS --iverilog "${VHDL_IVERILOG_EXECUTABLE}" --vvp "${VHDL_VVP_EXECUTABLE}") diff --git a/test/nl/formats/vhdl/VHDLConstructorTest.cpp b/test/nl/formats/vhdl/VHDLConstructorTest.cpp index dbf9c6e8a..6bfa99f37 100644 --- a/test/nl/formats/vhdl/VHDLConstructorTest.cpp +++ b/test/nl/formats/vhdl/VHDLConstructorTest.cpp @@ -1237,6 +1237,114 @@ end; } } +TEST_F(VHDLConstructorTest, ConstantFoldingAndCombinationalSharing) { + auto* design = VHDLConstructor(library_).construct(R"( +entity folded is port(a, b, s : in bit; v : in bit_vector(3 downto 0); + y : out bit_vector(11 downto 0)); end; +architecture rtl of folded is begin + y(0) <= a and '0'; y(1) <= '1' and a; + y(2) <= '0' or a; y(3) <= a or '1'; + y(4) <= a xnor '0'; y(5) <= '1' xnor a; + y(6) <= a when true else b; + y(7) <= a when s = '1' else a; + y(8) <= a and b; y(9) <= b and a; + y(10) <= '1' when v = "0010" else '0'; + y(11) <= '1' when v = "0010" else '0'; +end; +)"); + size_t inverters = 0, reductions = 0, ands = 0, muxes = 0; + for (auto* instance : design->getInstances()) { + const auto* model = instance->getModel(); + if (NLDB0::isMux2(model)) ++muxes; + if (!NLDB0::isGate(model)) continue; + if (NLDB0::getGateName(model) == "not") ++inverters; + if (NLDB0::getGateName(model) == "and") { + ++ands; + if (NLDB0::getGateNTerms(model)->getWidth() == 4) ++reductions; + } + EXPECT_NE(NLDB0::getGateName(model), "xnor"); + } + EXPECT_EQ(inverters, 4u); + EXPECT_EQ(ands, 2u); + EXPECT_EQ(reductions, 1u); + EXPECT_EQ(muxes, 1u); + for (unsigned pattern = 0; pattern < 128; ++pattern) { + std::unordered_map values; + for (const auto& [name, shift] : {std::pair{"a", 0u}, {"b", 1u}, {"s", 2u}}) + values[design->getScalarTerm(NLName(name))->getNet()] = (pattern >> shift) & 1; + const auto v = pattern >> 3; + for (unsigned bit = 0; bit < 4; ++bit) + values[design->getBusTerm(NLName("v"))->getBit(bit)->getNet()] = (v >> bit) & 1; + const bool a = pattern & 1, b = pattern & 2; + const bool expected[]{false, a, a, true, !a, a, a, a, a && b, a && b, v == 2, v == 2}; + std::unordered_set visiting; + for (unsigned bit = 0; bit < 12; ++bit) + EXPECT_EQ(evaluateRTL(design->getBusTerm(NLName("y"))->getBit(bit)->getNet(), values, visiting), expected[bit]); + } + if (const auto* directory = std::getenv("VHDL_TABLE_DUMP")) { + SNLVRLDumper dumper; + dumper.setSingleFile(true); + dumper.setTopFileName("folded.v"); + dumper.dumpDesign(design, directory); + } +} + +TEST_F(VHDLConstructorTest, IndexedTablePrimitivesAndSharing) { + auto* design = VHDLConstructor(library_).constructFile(SNL_VHDL_INDEXED_TABLES); + ASSERT_NE(design, nullptr); + size_t tables = 0, constantTables = 0, muxes = 0, gates = 0, flops = 0; + for (auto* instance : design->getInstances()) { + auto* model = instance->getModel(); + if (NLDB0::isTableSelect(model)) { + ++tables; + const auto signature = NLDB0::getTableSelectSignature(instance); + EXPECT_EQ(signature.width, 8u); + EXPECT_EQ(signature.depth, 3u); + EXPECT_TRUE(signature.abits == 2 || signature.abits == 3); + bool constants = true; + for (auto* bit : NLDB0::getTableSelectData(model)->getBits()) { + auto* net = instance->getInstTerm(bit)->getNet(); + constants &= net->isConstant0() || net->isConstant1(); + } + constantTables += constants; + } else if (NLDB0::isMux2(model)) ++muxes; + else if (NLDB0::isDFF(model)) ++flops; + else if (NLDB0::isGate(model)) ++gates; + else FAIL() << "unexpected primitive: " << model->getName().getString(); + } + // Three equivalent zero-based reads and both offset directions share. + // The late-driven ROM still has constant DATA, independently of statement order. + EXPECT_EQ(tables, 5u); + EXPECT_EQ(constantTables, 4u); + EXPECT_EQ(muxes, 0u); + EXPECT_EQ(flops, 3u); + EXPECT_LT(gates, 90u); + const auto flopped = dffBits(design); + ASSERT_EQ(flopped.size(), 24u); + for (unsigned pattern = 0; pattern < 256; ++pattern) { + std::unordered_map values; + for (unsigned bit = 0; bit < 8; ++bit) + values[design->getBusTerm(NLName("d"))->getBit(bit)->getNet()] = (pattern >> bit) & 1; + std::unordered_set visiting; + for (const auto& flop : flopped) values[flop.output] = evaluateRTL(flop.data, values, visiting); + for (unsigned address = 0; address < 3; ++address) { + auto current = values; + for (unsigned bit = 0; bit < 3; ++bit) + current[design->getBusTerm(NLName("addr"))->getBit(bit)->getNet()] = (address >> bit) & 1; + const unsigned expected = address == 0 ? pattern : address == 1 ? (pattern ^ 255) : (pattern ^ 0xa5); + for (unsigned bit = 0; bit < 8; ++bit) + EXPECT_EQ(evaluateRTL(design->getBusTerm(NLName("writable_q"))->getBit(bit)->getNet(), current, visiting), + bool((expected >> bit) & 1)); + } + } + if (const auto* directory = std::getenv("VHDL_TABLE_DUMP")) { + SNLVRLDumper dumper; + dumper.setSingleFile(true); + dumper.setTopFileName("indexed_tables.v"); + dumper.dumpDesign(design, directory); + } +} + TEST_F(VHDLConstructorTest, PackageROMAndDynamicMemoryCycles) { VHDLConstructor constructor(library_); EXPECT_EQ(constructor.construct(R"( diff --git a/test/nl/formats/vhdl/VHDLTestUtils.h b/test/nl/formats/vhdl/VHDLTestUtils.h index c81f0540c..c7c245168 100644 --- a/test/nl/formats/vhdl/VHDLTestUtils.h +++ b/test/nl/formats/vhdl/VHDLTestUtils.h @@ -75,7 +75,15 @@ inline bool evaluateRTL(SNLBitNet* net, std::unordered_map& va return evaluateRTL(driver->getInstTerm(term)->getNet(), values, visiting, memories); }; bool value; - if (NLDB0::isMemory(model)) { + if (NLDB0::isTableSelect(model)) { + const auto signature = NLDB0::getTableSelectSignature(driver); + size_t address = 0; + for (size_t i = signature.abits; i; --i) + address = address * 2 + read(NLDB0::getTableSelectAddress(model)->getBit(i - 1)); + if (address >= signature.depth) throw std::runtime_error("unknown table select result"); + const auto bit = static_cast(output)->getBit(); + value = read(NLDB0::getTableSelectData(model)->getBit(address * signature.width + bit)); + } else if (NLDB0::isMemory(model)) { if (!memories) throw std::runtime_error("missing memory state"); const auto signature = NLDB0::getMemorySignature(driver); const auto bit = static_cast(output)->getBit(); diff --git a/test/nl/formats/vhdl/compare_indexed_tables.py b/test/nl/formats/vhdl/compare_indexed_tables.py new file mode 100644 index 000000000..3bac1f039 --- /dev/null +++ b/test/nl/formats/vhdl/compare_indexed_tables.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 +"""Exercise table ordering, writable DATA and invalid addresses in dumped Verilog.""" +import argparse +import os +from pathlib import Path +import tempfile + +from compare_pipeline import run + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--adapter", required=True) + parser.add_argument("--iverilog", required=True) + parser.add_argument("--vvp", required=True) + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="naja-vhdl-tables-") as directory: + work = Path(directory) + run([str(Path(args.adapter).resolve()), + "--gtest_filter=VHDLConstructorTest.IndexedTablePrimitivesAndSharing:" + "VHDLConstructorTest.ConstantFoldingAndCombinationalSharing"], + work, dict(os.environ, VHDL_TABLE_DUMP=str(work))) + work.joinpath("tb.v").write_text(""" +module tb; + reg clk = 0; + reg [2:0] addr; + reg [0:0] narrow; + reg [7:0] d; + wire [7:0] direct_q, signal_q, duplicate_q, late_q, writable_q; + wire [7:0] offset_q, descending_q, narrow_q; + reg [7:0] expected, written, offset_expected; + integer pattern, a; + indexed_tables dut(.*); + reg fa, fb, fs; + reg [3:0] fv; + wire [11:0] folded_y; + reg [11:0] folded_expected; + folded folded_dut(.a(fa), .b(fb), .s(fs), .v(fv), .y(folded_y)); + function [7:0] rom(input integer index); + case (index) + 0: rom = 8'h52; + 1: rom = 8'h09; + 2: rom = 8'hA6; + default: rom = 8'hxx; + endcase + endfunction + initial begin + for (pattern = 0; pattern < 256; pattern = pattern + 1) begin + d = pattern; clk = 0; #5; clk = 1; #5; + for (a = 0; a < 8; a = a + 1) begin + addr = a; narrow = a & 1; #1; + expected = rom(a); offset_expected = rom(a - 3); + case (a) + 0: written = d; + 1: written = ~d; + 2: written = d ^ 8'hA5; + default: written = 8'hxx; + endcase + if (direct_q !== expected || signal_q !== expected || + duplicate_q !== expected || late_q !== expected || + writable_q !== written || offset_q !== offset_expected || + descending_q !== offset_expected || narrow_q !== rom(a & 1)) + $fatal(1, "table mismatch: pattern=%0d address=%0d", pattern, a); + end + end + addr = 3'bxxx; #1; + if (direct_q !== 8'hxx || writable_q !== 8'hxx || offset_q !== 8'hxx) + $fatal(1, "unknown address did not produce X"); + addr = 3'bzzz; #1; + if (direct_q !== 8'hxx || writable_q !== 8'hxx || descending_q !== 8'hxx) + $fatal(1, "high-impedance address did not produce X"); + for (pattern = 0; pattern < 128; pattern = pattern + 1) begin + fa = pattern & 1; fb = (pattern >> 1) & 1; + fs = (pattern >> 2) & 1; fv = pattern >> 3; #1; + folded_expected = {fv == 2, fv == 2, fa & fb, fa & fb, + fa, fa, fa, ~fa, 1'b1, fa, fa, 1'b0}; + if (folded_y !== folded_expected) + $fatal(1, "folded output mismatch: pattern=%0d", pattern); + end + $display("PASS: 2048 table vectors plus X/Z addresses"); + $finish; + end +endmodule +""") + run([args.iverilog, "-g2012", "-s", "tb", "-o", "sim", + "indexed_tables.v", "folded.v", "naja_primitives.v", "tb.v"], work) + run([args.vvp, "sim"], work) + print("Indexed table Verilog passed ordering, data and bounds checks.") + + +if __name__ == "__main__": + main() diff --git a/test/nl/formats/vhdl/indexed_tables.vhd b/test/nl/formats/vhdl/indexed_tables.vhd new file mode 100644 index 000000000..911c76dd4 --- /dev/null +++ b/test/nl/formats/vhdl/indexed_tables.vhd @@ -0,0 +1,42 @@ +-- SPDX-FileCopyrightText: 2026 The Naja authors +-- SPDX-License-Identifier: Apache-2.0 +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity indexed_tables is + port(clk : in std_logic; + addr : in std_logic_vector(2 downto 0); + narrow : in std_logic_vector(0 downto 0); + d : in std_logic_vector(7 downto 0); + direct_q, signal_q, duplicate_q, late_q, writable_q, + offset_q, descending_q, narrow_q : out std_logic_vector(7 downto 0)); +end; + +architecture rtl of indexed_tables is + type table_type is array (0 to 2) of std_logic_vector(7 downto 0); + type offset_type is array (3 to 5) of std_logic_vector(7 downto 0); + type descending_type is array (5 downto 3) of std_logic_vector(0 to 7); + constant rom : table_type := (x"52", x"09", x"A6"); + constant offset_rom : offset_type := (x"52", x"09", x"A6"); + constant descending_rom : descending_type := (x"A6", x"09", x"52"); + signal rom_signal, late_signal, writable : table_type; +begin + rom_signal <= rom; + direct_q <= rom(to_integer(unsigned(addr))); + signal_q <= rom_signal(to_integer(unsigned(addr))); + duplicate_q <= rom_signal(to_integer(unsigned(addr))); + late_q <= late_signal(to_integer(unsigned(addr))); + late_signal <= rom; + writable_q <= writable(to_integer(unsigned(addr))); + offset_q <= offset_rom(to_integer(unsigned(addr))); + descending_q <= descending_rom(to_integer(unsigned(addr))); + narrow_q <= rom(to_integer(unsigned(narrow))); + process(clk) begin + if rising_edge(clk) then + writable(0) <= d; + writable(1) <= not d; + writable(2) <= d xor x"A5"; + end if; + end process; +end; From f3c3d6794185139ada37f5884cd62a67f4f9c66e Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 27 Sep 2026 11:35:50 +0200 Subject: [PATCH 07/11] Add optional schematic viewer extra and import shim --- pyproject.toml | 3 ++ src/najaeda/README.rst | 4 ++ .../najaeda/docs/source/introduction.rst | 4 ++ src/najaeda/najaeda/schematic.py | 14 ++++++ test/najaeda/test_najaeda_schematic.py | 44 +++++++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 src/najaeda/najaeda/schematic.py create mode 100644 test/najaeda/test_najaeda_schematic.py diff --git a/pyproject.toml b/pyproject.toml index 65eae3279..f091cc84e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,9 @@ classifiers = [ "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)" ] +[project.optional-dependencies] +schematic = ["naja-schematic>=0.1.1"] + [tool.scikit-build.cmake.define] CMAKE_BUILD_TYPE="Release" BUILD_NAJA_PYTHON="ON" diff --git a/src/najaeda/README.rst b/src/najaeda/README.rst index bb103078d..f99759329 100644 --- a/src/najaeda/README.rst +++ b/src/najaeda/README.rst @@ -58,6 +58,10 @@ Installation pip install najaeda +For the optional `schematic viewer `_, +run ``pip install "najaeda[schematic]"``. In a notebook, use +``from najaeda.schematic import show; show()`` to display the currently loaded design. + Requires Python 3.10 or later. Pre-built wheels are published for: * Linux x86_64 and AArch64 (``manylinux_2_28``; glibc 2.28 or later) diff --git a/src/najaeda/najaeda/docs/source/introduction.rst b/src/najaeda/najaeda/docs/source/introduction.rst index 71fa1b41f..92004f545 100644 --- a/src/najaeda/najaeda/docs/source/introduction.rst +++ b/src/najaeda/najaeda/docs/source/introduction.rst @@ -53,6 +53,10 @@ Installation pip install najaeda +For the optional `schematic viewer `_, +run ``pip install "najaeda[schematic]"``. In a notebook, use +``from najaeda.schematic import show; show()`` to display the currently loaded design. + Requires Python 3.10 or later. Pre-built wheels are published for: * Linux x86_64 and AArch64 (``manylinux_2_28``; glibc 2.28 or later) diff --git a/src/najaeda/najaeda/schematic.py b/src/najaeda/najaeda/schematic.py new file mode 100644 index 000000000..8bf06b05b --- /dev/null +++ b/src/najaeda/najaeda/schematic.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# +# SPDX-License-Identifier: Apache-2.0 + +"""Optional schematic viewer: https://github.com/najaeda/naja-schematic.""" + +try: + from naja_schematic import show, diagnosis_response, handle_request, __version__ +except ImportError as e: + raise ImportError( + 'The schematic viewer requires pip install "najaeda[schematic]"' + ) from e + +__all__ = ["show", "diagnosis_response", "handle_request", "__version__"] diff --git a/test/najaeda/test_najaeda_schematic.py b/test/najaeda/test_najaeda_schematic.py new file mode 100644 index 000000000..57e19d72a --- /dev/null +++ b/test/najaeda/test_najaeda_schematic.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import sys +import unittest +from unittest.mock import patch + +import najaeda + + +class NajaedaSchematicTest(unittest.TestCase): + def setUp(self): + self.previous_module = sys.modules.pop("najaeda.schematic", None) + self.previous_attribute = najaeda.__dict__.pop("schematic", None) + + def tearDown(self): + sys.modules.pop("najaeda.schematic", None) + najaeda.__dict__.pop("schematic", None) + if self.previous_module is not None: + sys.modules["najaeda.schematic"] = self.previous_module + if self.previous_attribute is not None: + najaeda.schematic = self.previous_attribute + + def test_missing_viewer(self): + with patch.dict(sys.modules, {"naja_schematic": None}): + with self.assertRaisesRegex(ImportError, r"najaeda\[schematic\]") as error: + importlib.import_module("najaeda.schematic") + self.assertIsInstance(error.exception.__cause__, ImportError) + + def test_public_api(self): + try: + import naja_schematic + except ImportError: + self.skipTest("naja-schematic is not installed") + schematic = importlib.import_module("najaeda.schematic") + for name in ("show", "diagnosis_response", "handle_request", "__version__"): + with self.subTest(name=name): + self.assertIs(getattr(schematic, name), getattr(naja_schematic, name)) + + +if __name__ == "__main__": + unittest.main() From 472f5f8fa183db3c06d6ffe1d27201d6dbfad8da Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 27 Sep 2026 15:48:02 +0200 Subject: [PATCH 08/11] coverage and cleaning --- src/najaeda/najaeda/remote/__init__.py | 0 src/najaeda/najaeda/remote/serialization.py | 160 ---------- src/najaeda/najaeda/remote/server.py | 310 ------------------- test/najaeda/test_najaeda_schematic.py | 13 + test/najaeda/test_najaeda_serialization.py | 173 ----------- test/nl/formats/vhdl/VHDLConstructorTest.cpp | 31 ++ 6 files changed, 44 insertions(+), 643 deletions(-) delete mode 100644 src/najaeda/najaeda/remote/__init__.py delete mode 100644 src/najaeda/najaeda/remote/serialization.py delete mode 100644 src/najaeda/najaeda/remote/server.py delete mode 100644 test/najaeda/test_najaeda_serialization.py diff --git a/src/najaeda/najaeda/remote/__init__.py b/src/najaeda/najaeda/remote/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/najaeda/najaeda/remote/serialization.py b/src/najaeda/najaeda/remote/serialization.py deleted file mode 100644 index 7302e2f70..000000000 --- a/src/najaeda/najaeda/remote/serialization.py +++ /dev/null @@ -1,160 +0,0 @@ -# SPDX-FileCopyrightText: 2024 The Naja authors -# -# -# SPDX-License-Identifier: Apache-2.0 - -""" -Serialization helpers for exposing NajaEDA objects over the WebSocket API. -These functions convert internal objects into JSON-serializable dicts. -""" - -from typing import List, Optional, TypedDict, Union -from najaeda import naja - - -def direction_to_int(direction: naja.SNLTerm.Direction) -> int: - """ - Convert an SNLTerm.Direction enum to a compact integer representation: - 0 -> Input - 1 -> Output - 2 -> InOut / others - """ - if direction == naja.SNLTerm.Direction.Input: - return 0 - elif direction == naja.SNLTerm.Direction.Output: - return 1 - else: - return 2 - - -class SerializedDesignRef(TypedDict): - db_id: int - library_id: int - design_id: int - - -def serialize_design_ref(model: naja.SNLDesign) -> SerializedDesignRef: - """ - Serialize a design reference into db / library / design IDs. - """ - return { - "db_id": model.getDB().getID(), - "library_id": model.getLibrary().getID(), - "design_id": model.getID(), - } - - -class SerializedModel(TypedDict): - name: str - child_id: int - model_name: str - design_ref: SerializedDesignRef - has_terms: bool - has_primitives: bool - has_instances: bool - - -def serialize_model(model: naja.SNLDesign, child_id: int, name: str) -> SerializedModel: - """ - Serialize a model (SNLDesign) plus its position as a child instance (child_id, name). - """ - return { - "name": name, - "child_id": child_id, - "model_name": model.getName(), - "design_ref": serialize_design_ref(model), - "has_terms": model.hasTerms(), - "has_primitives": model.hasPrimitiveInstances(), - "has_instances": model.hasNonPrimitiveInstances(), - } - - -class SerializedTerm(TypedDict, total=False): - name: str - child_id: int - direction: int - kind: str - msb: Optional[int] - lsb: Optional[int] - bit: Optional[int] - members: List["SerializedTerm"] - - -def serialize_term(term: naja.SNLTerm) -> SerializedTerm: - """ - Serialize a top-level term (design term). - """ - msb = None - lsb = None - bit = None - - kind = "scalar" - members = None - - if isinstance(term, naja.SNLBundleTerm): - kind = "bundle" - members = [serialize_term(member) for member in term.getMembers()] - elif isinstance(term, naja.SNLBusTerm): - kind = "bus" - msb = term.getMSB() - lsb = term.getLSB() - - if isinstance(term, naja.SNLBusTermBit): - bit = term.getBit() - - payload: SerializedTerm = { - "name": term.getName(), - "child_id": term.getID(), - "direction": direction_to_int(term.getDirection()), - "kind": kind, - "msb": msb, - "lsb": lsb, - "bit": bit, - } - if members is not None: - payload["members"] = members - return payload - - -class SerializedOccurrence(TypedDict): - path: List[List[Union[str, int]]] - term_id: int - name: str - direction: int - bit: Optional[int] - - -def serialize_equipotential_occurrence(occ: naja.SNLOccurrence) -> SerializedOccurrence: - """ - Serialize a SNLOccurrence for an inst-term that belongs to an equipotential. - """ - path = [[inst.getName(), inst.getID()] for inst in occ.getPath().getInstances()] - inst_term = occ.getInstTerm() - inst = inst_term.getInstance() - - # Last element is the instance hosting the term - path.append([inst.getName(), inst.getID()]) - - term = inst_term.getBitTerm() - bit = term.getBit() if isinstance(term, naja.SNLBusTermBit) else None - - return { - "path": path, - "term_id": term.getID(), - "name": term.getName(), - "direction": direction_to_int(term.getDirection()), - "bit": bit, - } - - -def serialize_equipotential_term(term: naja.SNLTerm): - """ - Serialize a top-level term belonging to an equipotential. - """ - bit = term.getBit() if isinstance(term, naja.SNLBusTermBit) else None - return { - "name": term.getName(), - "child_id": term.getID(), - "direction": direction_to_int(term.getDirection()), - "bit": bit, - } diff --git a/src/najaeda/najaeda/remote/server.py b/src/najaeda/najaeda/remote/server.py deleted file mode 100644 index ff15c7c3f..000000000 --- a/src/najaeda/najaeda/remote/server.py +++ /dev/null @@ -1,310 +0,0 @@ -# SPDX-FileCopyrightText: 2024 The Naja authors -# -# -# SPDX-License-Identifier: Apache-2.0 - -""" -WebSocket protocol layer for najaeda remote access. - -This module knows how to: -- interpret incoming JSON requests -- talk to NLUniverse / SNL objects -- return JSON-serializable responses via the websocket -""" - -import json -import logging -from typing import Any, Dict, List, Optional, Tuple - -from najaeda import naja - -from .serialization import ( - serialize_equipotential_occurrence, - serialize_equipotential_term, - serialize_model, - serialize_term, -) - -logger = logging.getLogger(__name__) - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def get_design_ref(ref_msg: Optional[Dict[str, Any]]) -> Optional[Tuple[int, int, int]]: - if not ref_msg: - return None - return ( - ref_msg.get("db_id"), - ref_msg.get("library_id"), - ref_msg.get("design_id"), - ) - - -def get_path_from_ids(top: naja.SNLDesign, instance_ids: List[int]) -> naja.SNLPath: - current = top - path = naja.SNLPath() - for inst_id in instance_ids: - instance = current.getInstanceByID(inst_id) - if not instance: - raise ValueError(f"Instance with ID {inst_id} not found in path resolution") - path = naja.SNLPath(path, instance) - current = instance.getModel() - return path - - -async def send_error( - websocket, - response_type: str, - gui_id: int = 0, - message: Optional[str] = None, -): - payload: Dict[str, Any] = { - "response": response_type, - "gui_id": gui_id, - "error": True, - } - if message is not None: - payload["message"] = message - await websocket.send(json.dumps(payload)) - - -# --------------------------------------------------------------------------- -# Request handlers -# --------------------------------------------------------------------------- - -async def handle_load_root(websocket, gui_id: int): - u = naja.NLUniverse.get() - top = u.getTopDesign() - if not top: - await send_error(websocket, "root_response", gui_id, "Top design not set") - return - - await websocket.send( - json.dumps( - { - "response": "root_response", - "gui_id": gui_id, - "root": serialize_model(top, 0, top.getName()), - } - ) - ) - - -async def handle_load_instance_like( - websocket, - req_type: str, - design_ref_msg: Dict[str, Any], - gui_id: int, -): - ref = get_design_ref(design_ref_msg) - if ref is None: - await send_error(websocket, f"{req_type}_response", gui_id, "Missing design_ref") - return - - u = naja.NLUniverse.get() - design = u.getSNLDesign(ref) - logger.debug("Handling %s for design_ref=%s -> %s", req_type, ref, design) - - if not design: - await send_error(websocket, f"{req_type}_response", gui_id, "Design not found") - return - - if req_type == "load_instance": - await websocket.send( - json.dumps( - { - "response": "instance_response", - "gui_id": gui_id, - "instance": { - "design_ref": { - "db_id": design.getDB().getID(), - "library_id": design.getLibrary().getID(), - "design_id": design.getID(), - }, - "has_terms": design.hasTerms(), - "has_primitives": design.hasPrimitiveInstances(), - "has_instances": design.hasNonPrimitiveInstances(), - }, - } - ) - ) - return - - if req_type in {"load_primitives", "load_instances"}: - children = [] - instances = ( - design.getPrimitiveInstances() - if req_type == "load_primitives" - else design.getNonPrimitiveInstances() - ) - - for instance in instances: - model = instance.getModel() - children.append(serialize_model(model, instance.getID(), instance.getName())) - - response_type = req_type.replace("load_", "") + "_response" - await websocket.send( - json.dumps( - { - "response": response_type, - "gui_id": gui_id, - "children": children, - } - ) - ) - return - - if req_type == "load_terms": - terms_payload = [serialize_term(term) for term in design.getTerms()] - await websocket.send( - json.dumps( - { - "response": "terms_response", - "gui_id": gui_id, - "children": terms_payload, - } - ) - ) - return - - -async def handle_load_equipotential(websocket, request: Dict[str, Any], gui_id: int): - path_ids: List[int] = request.get("path", []) or [] - term_id = request.get("term_id") - bit = request.get("bit", None) - - if term_id is None: - await send_error(websocket, "equipotential_response", gui_id, "Missing term_id") - return - - logger.debug( - "LoadEquipotential path=%s term_id=%s bit=%s", path_ids, term_id, bit - ) - - u = naja.NLUniverse.get() - top = u.getTopDesign() - if top is None: - await send_error( - websocket, "equipotential_response", gui_id, "Top design not set" - ) - return - - path = get_path_from_ids(top, path_ids) if path_ids else naja.SNLPath() - logger.debug("Resolved path: %s", path) - - if path.empty(): - design = top - term = design.getTermByID(term_id) - if term is None: - await send_error( - websocket, "equipotential_response", gui_id, "Term not found" - ) - return - - if bit is not None: - if not isinstance(term, naja.SNLBusTerm): - await send_error( - websocket, - "equipotential_response", - gui_id, - "Bit index provided but term is not a bus", - ) - return - start_point = term.getBusTermBit(bit) - else: - start_point = term - else: - design = path.getModel() - term = design.getTermByID(term_id) - if term is None: - await send_error( - websocket, "equipotential_response", gui_id, "Term not found" - ) - return - - instance = path.getTailInstance() - if bit is not None: - if not isinstance(term, naja.SNLBusTerm): - await send_error( - websocket, - "equipotential_response", - gui_id, - "Bit index provided but term is not a bus", - ) - return - term = term.getBusTermBit(bit) - - inst_term = instance.getInstTerm(term) - head_path = path.getHeadPath() - start_point = naja.SNLOccurrence(head_path, inst_term) - - logger.debug("Start point for equipotential: %s", start_point) - - equipotential = naja.SNLEquipotential(start_point) - - occurrences = [ - serialize_equipotential_occurrence(occ) - for occ in equipotential.getInstTermOccurrences() - ] - terms = [serialize_equipotential_term(t) for t in equipotential.getTerms()] - - await websocket.send( - json.dumps( - { - "response": "equipotential_response", - "gui_id": gui_id, - "occurrences": occurrences, - "terms": terms, - } - ) - ) - - -# --------------------------------------------------------------------------- -# Main entry point for the connection handler -# --------------------------------------------------------------------------- - -async def handle_request(websocket, request: Dict[str, Any]): - """ - Entry point used by the connection-level handler in server.py. - - It: - - inspects the "request" field - - dispatches to the appropriate handler - - ensures consistent error responses - """ - req_type = request.get("request") - design_ref_message = request.get("design_ref") - gui_id = request.get("gui_id", 0) - - logger.debug("Received request type=%s gui_id=%s", req_type, gui_id) - - if req_type is None: - await send_error(websocket, "unknown_response", gui_id, "Missing 'request' field") - return - - if req_type == "load_root": - await handle_load_root(websocket, gui_id) - return - - if req_type in {"load_instance", "load_primitives", "load_instances", "load_terms"}: - if not design_ref_message: - await send_error( - websocket, - f"{req_type}_response", - gui_id, - "Missing 'design_ref' for this request type", - ) - return - await handle_load_instance_like(websocket, req_type, design_ref_message, gui_id) - return - - if req_type == "load_equipotential": - await handle_load_equipotential(websocket, request, gui_id) - return - - # Unknown request type - logger.warning("Unknown request type: %s", req_type) - await send_error(websocket, "unknown_response", gui_id, f"Unknown request '{req_type}'") diff --git a/test/najaeda/test_najaeda_schematic.py b/test/najaeda/test_najaeda_schematic.py index 57e19d72a..c53976984 100644 --- a/test/najaeda/test_najaeda_schematic.py +++ b/test/najaeda/test_najaeda_schematic.py @@ -4,6 +4,7 @@ import importlib import sys +from types import ModuleType import unittest from unittest.mock import patch @@ -39,6 +40,18 @@ def test_public_api(self): with self.subTest(name=name): self.assertIs(getattr(schematic, name), getattr(naja_schematic, name)) + def test_public_api_with_stub_viewer(self): + viewer = ModuleType("naja_schematic") + exports = ["show", "diagnosis_response", "handle_request", "__version__"] + for name in exports: + setattr(viewer, name, object()) + with patch.dict(sys.modules, {"naja_schematic": viewer}): + schematic = importlib.import_module("najaeda.schematic") + self.assertEqual(schematic.__all__, exports) + for name in exports: + with self.subTest(name=name): + self.assertIs(getattr(schematic, name), getattr(viewer, name)) + if __name__ == "__main__": unittest.main() diff --git a/test/najaeda/test_najaeda_serialization.py b/test/najaeda/test_najaeda_serialization.py deleted file mode 100644 index 27c0149ab..000000000 --- a/test/najaeda/test_najaeda_serialization.py +++ /dev/null @@ -1,173 +0,0 @@ -# SPDX-FileCopyrightText: 2023 The Naja authors -# -# SPDX-License-Identifier: Apache-2.0 - -import unittest -from pathlib import Path -from najaeda.remote.serialization import ( - direction_to_int, - serialize_design_ref, - serialize_model, - serialize_term, - serialize_equipotential_term, - serialize_equipotential_occurrence, -) -from najaeda import naja - -class TestSerialization(unittest.TestCase): - - @staticmethod - def _bundle_issue_120_lib(): - return ( - Path(__file__).resolve().parents[2] - / "test" - / "nl" - / "formats" - / "liberty" - / "benchmarks" - / "tests" - / "bundle_issue_120.lib" - ) - - def setUp(self): - u = naja.NLUniverse.create() - db = naja.NLDB.create(u) - self.lib = naja.NLLibrary.create(db, "TestLib") - self.design = naja.SNLDesign.create(self.lib, "TestDesign") - - def tearDown(self): - naja.NLUniverse.get().destroy() - - # ------------------------------------------------------------ - # direction_to_int - # ------------------------------------------------------------ - def test_direction_to_int(self): - self.assertEqual(direction_to_int(naja.SNLTerm.Direction.Input), 0) - self.assertEqual(direction_to_int(naja.SNLTerm.Direction.Output), 1) - self.assertEqual(direction_to_int(naja.SNLTerm.Direction.InOut), 2) - - # ------------------------------------------------------------ - # serialize_design_ref - # ------------------------------------------------------------ - def test_serialize_design_ref(self): - ref = serialize_design_ref(self.design) - nlid = self.design.getNLID() - self.assertEqual(ref["db_id"], nlid.getDBID()) - self.assertEqual(ref["library_id"], nlid.getLibraryID()) - self.assertEqual(ref["design_id"], nlid.getDesignID()) - - # ------------------------------------------------------------ - # serialize_model - # ------------------------------------------------------------ - def test_serialize_model(self): - payload = serialize_model(self.design, child_id=7, name="TOP") - self.assertEqual(payload["name"], "TOP") - self.assertEqual(payload["child_id"], 7) - self.assertEqual(payload["model_name"], "TestDesign") - self.assertIn("design_ref", payload) - self.assertIn("has_terms", payload) - self.assertIn("has_primitives", payload) - self.assertIn("has_instances", payload) - - # ------------------------------------------------------------ - # serialize_term (scalar + bus + bus bit) - # ------------------------------------------------------------ - def test_serialize_scalar_term(self): - term = naja.SNLScalarTerm.create( - self.design, - naja.SNLTerm.Direction.Input, - "A" - ) - p = serialize_term(term) - self.assertEqual(p["name"], "A") - self.assertEqual(p["direction"], 0) - self.assertIsNone(p["msb"]) - self.assertIsNone(p["lsb"]) - self.assertIsNone(p["bit"]) - - def test_serialize_bus_term(self): - bus = naja.SNLBusTerm.create( - self.design, - naja.SNLTerm.Direction.Output, - 7, 0, - "BUS" - ) - p = serialize_term(bus) - self.assertEqual(p["msb"], 7) - self.assertEqual(p["lsb"], 0) - self.assertIsNone(p["bit"]) - - def test_serialize_bus_term_bit(self): - bus = naja.SNLBusTerm.create( - self.design, - naja.SNLTerm.Direction.Output, - 3, 0, - "BUS" - ) - bit2 = bus.getBusTermBit(2) - p = serialize_term(bit2) - self.assertEqual(p["bit"], 2) - - def test_serialize_bundle_term(self): - self.design.getDB().loadLibertyPrimitives([str(self._bundle_issue_120_lib())]) - primitive_lib = next(iter(self.design.getDB().getPrimitiveLibraries())) - primitive = primitive_lib.getSNLDesign("cell_def") - payload = serialize_term(primitive.getBundleTerm("D")) - self.assertEqual(payload["kind"], "bundle") - self.assertEqual(payload["name"], "D") - self.assertEqual(["D0", "D1"], [member["name"] for member in payload["members"]]) - self.assertEqual("scalar", payload["members"][0]["kind"]) - self.assertEqual("scalar", payload["members"][1]["kind"]) - self.assertIsNone(payload["members"][1]["msb"]) - self.assertIsNone(payload["members"][1]["lsb"]) - - # ------------------------------------------------------------ - # serialize_equipotential_term - # ------------------------------------------------------------ - def test_serialize_equipotential_term(self): - term = naja.SNLScalarTerm.create( - self.design, - naja.SNLTerm.Direction.Input, - "CLK" - ) - p = serialize_equipotential_term(term) - self.assertEqual(p["name"], "CLK") - self.assertEqual(p["direction"], 0) - - # ------------------------------------------------------------ - # serialize_equipotential_occurrence - # ------------------------------------------------------------ - def test_serialize_equipotential_occurrence(self): - mid = naja.SNLDesign.create(self.lib, "Mid") - leaf = naja.SNLDesign.create(self.lib, "Leaf") - leaf_in = naja.SNLScalarTerm.create( - leaf, - naja.SNLTerm.Direction.Input, - "IN" - ) - - inst1 = naja.SNLInstance.create(self.design, mid, "inst1") - inst2 = naja.SNLInstance.create(mid, leaf, "inst2") - - path0 = naja.SNLPath() - path1 = naja.SNLPath(path0, inst1) - inst_terms = tuple(inst2.getInstTerms()) - inst_term = inst_terms[0] - occ = naja.SNLOccurrence(path1, inst_term) - - payload = serialize_equipotential_occurrence(occ) - self.assertEqual( - payload["path"], - [ - [inst1.getName(), inst1.getID()], - [inst2.getName(), inst2.getID()], - ], - ) - self.assertEqual(payload["term_id"], leaf_in.getID()) - self.assertEqual(payload["name"], leaf_in.getName()) - self.assertEqual(payload["direction"], direction_to_int(leaf_in.getDirection())) - self.assertIsNone(payload["bit"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/nl/formats/vhdl/VHDLConstructorTest.cpp b/test/nl/formats/vhdl/VHDLConstructorTest.cpp index 6bfa99f37..b7a9f342e 100644 --- a/test/nl/formats/vhdl/VHDLConstructorTest.cpp +++ b/test/nl/formats/vhdl/VHDLConstructorTest.cpp @@ -1496,6 +1496,37 @@ architecture rtl of lookup is begin y <= rom(conv_integer(a)); end; } } +TEST_F(VHDLConstructorTest, UnsupportedConversionOperandsDoNotPublishDesigns) { + for (const auto& [conversion, operandType, resultType, diagnostic] : + std::vector>{ + {"unsigned", "std_logic", "unsigned(0 downto 0)", "unsupported vector conversion operand"}, + {"signed", "bit_vector(3 downto 0)", "signed(3 downto 0)", "unsupported vector conversion operand"}, + {"std_logic_vector", "bit_vector(3 downto 0)", "std_logic_vector(3 downto 0)", + "unsupported vector conversion operand"}, + {"to_integer", "std_logic", "integer range 0 to 3", "unsupported to_integer argument"}, + {"to_integer", "std_logic_vector(1 downto 0)", "integer range 0 to 3", "unsupported to_integer argument"}, + {"to_integer", "unsigned(31 downto 0)", "integer range 0 to 3", "unsupported to_integer argument"}, + {"conv_integer", "std_logic", "integer range 0 to 3", "unsupported conv_integer argument"}, + {"conv_integer", "unsigned(1 downto 0)", "integer range 0 to 3", "unsupported conv_integer argument"}, + {"conv_integer", "std_logic_vector(31 downto 0)", "integer range 0 to 3", + "unsupported conv_integer argument"}}) { + SCOPED_TRACE(conversion + "(" + operandType + ")"); + const auto source = + "library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; " + "use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; " + "entity conversion is port(a : in " + operandType + "); end; " + "architecture rtl of conversion is signal y : " + resultType + "; " + "begin y <= " + conversion + "(a); end;"; + try { + VHDLConstructor(library_).construct(source); + FAIL() << "unsupported conversion accepted"; + } catch (const NLException& error) { + EXPECT_NE(std::string(error.what()).find(diagnostic), std::string::npos) << error.what(); + } + EXPECT_TRUE(library_->getSNLDesigns().empty()); + } +} + TEST_F(VHDLConstructorTest, UnsignedArithmeticWidthsAndDirections) { auto* design = VHDLConstructor(library_).construct(R"( library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; From 1285b3e5136d54d2c64f68cf9fc9f1bc22a41abb Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 27 Sep 2026 15:50:39 +0200 Subject: [PATCH 09/11] simplify versions --- .github/workflows/wheels.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 60d96c22c..003aa6e38 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -25,7 +25,7 @@ jobs: outputs: artifact_name: parser-generated-sources steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@v7 with: submodules: true @@ -162,16 +162,16 @@ jobs: steps: # build steps - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@v7 with: submodules: true - name: Setup Python - uses: actions/setup-python@v7.0.0 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Download parser-generated sources # if: runner.os == 'Windows' - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@v8 with: name: parser-generated-sources path: generated @@ -200,7 +200,7 @@ jobs: - name: Restore Windows dependency cache if: runner.os == 'Windows' id: vcpkg-cache - uses: actions/cache/restore@v6.1.0 + uses: actions/cache/restore@v6 with: path: ${{ runner.temp }}/vcpkg-binary-cache # Share native dependencies across Python versions; refresh on runner From f837bdf38605dbe08e663e132009c27305765ef1 Mon Sep 17 00:00:00 2001 From: xtof Date: Sun, 27 Sep 2026 22:20:30 +0200 Subject: [PATCH 10/11] fix MEM reset --- src/najaeda/najaeda/docs/source/raw_api.rst | 12 ++++++ .../netlist/decorators/SNLDesignModeling.cpp | 8 +++- test/najaeda/test_najaeda_system_verilog.py | 41 ++++++++++++++++++- .../SNLSVConstructorTestMemoryInference.cpp | 14 +++++++ .../snl/decorators/SNLDesignModelingTest0.cpp | 22 ++++++++++ test/nl/snl/kernel/NLDB0Test.cpp | 24 ++++++++--- 6 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/najaeda/najaeda/docs/source/raw_api.rst b/src/najaeda/najaeda/docs/source/raw_api.rst index d1ebf5531..42cfe81da 100644 --- a/src/najaeda/najaeda/docs/source/raw_api.rst +++ b/src/najaeda/najaeda/docs/source/raw_api.rst @@ -759,3 +759,15 @@ values with the reader build; regenerate snapshots after changing builds. .. autofunction:: najaeda.naja.intent_parameters_of .. autofunction:: najaeda.naja.intent_type_of .. autofunction:: najaeda.naja.intent_package_member + +Memory reset roles +------------------ + +For DB0 ``naja_mem__*`` primitives and other designs with a memory interface, +``SNLBitTerm.getRole()`` reports ``SNLTermRole.SyncReset`` for synchronous +resets and ``SNLTermRole.AsyncReset`` for asynchronous resets. Both satisfy +``is_reset()``; synchronous resets also satisfy ``is_sync_reset()``. +``getResetActiveLevel()`` returns ``SNLActiveLevel.Low`` for either active-low +reset mode and ``SNLActiveLevel.High`` for either active-high mode. With no +reset enabled, the reset pin has role ``Other`` and active level ``NA``. +The same queries on ``SNLInstTerm`` follow the model's bit term. diff --git a/src/nl/netlist/decorators/SNLDesignModeling.cpp b/src/nl/netlist/decorators/SNLDesignModeling.cpp index 6499682bf..b18385a1d 100644 --- a/src/nl/netlist/decorators/SNLDesignModeling.cpp +++ b/src/nl/netlist/decorators/SNLDesignModeling.cpp @@ -1699,6 +1699,11 @@ SNLDesignModeling::SNLTermRole getMemoryTermRole( memory.resetMode == ResetMode::AsyncHigh)) { return Role::AsyncReset; } + if (term == memory.reset && + (memory.resetMode == ResetMode::SyncLow || + memory.resetMode == ResetMode::SyncHigh)) { + return Role::SyncReset; + } auto contains = [term](const SNLDesignModeling::BitTerms& terms) { return std::find(terms.begin(), terms.end(), term) != terms.end(); }; @@ -1746,7 +1751,8 @@ SNLDesignModeling::SNLActiveLevel SNLDesignModeling::getResetActiveLevel( if (term && hasMemoryInterface(term->getDesign())) { auto memory = getMemoryInterface(term->getDesign()); if (term == memory.reset) { - return memory.resetMode == MemoryResetMode::AsyncLow + return (memory.resetMode == MemoryResetMode::AsyncLow || + memory.resetMode == MemoryResetMode::SyncLow) ? SNLActiveLevel::Low : SNLActiveLevel::High; } } diff --git a/test/najaeda/test_najaeda_system_verilog.py b/test/najaeda/test_najaeda_system_verilog.py index 0ac3d495c..2a880bfc7 100644 --- a/test/najaeda/test_najaeda_system_verilog.py +++ b/test/najaeda/test_najaeda_system_verilog.py @@ -7,7 +7,7 @@ import unittest import faulthandler -from najaeda import netlist +from najaeda import netlist, naja systemverilog_benchmarks = os.environ.get("SYSTEMVERILOG_BENCHMARKS_PATH") if not systemverilog_benchmarks: @@ -24,6 +24,45 @@ class NajaEDASystemVerilogTest(unittest.TestCase): def tearDown(self): netlist.reset() + def test_memory_sync_reset_role(self): + for condition, level in (("rst", naja.SNLActiveLevel.High), + ("~rst", naja.SNLActiveLevel.Low)): + with self.subTest(condition=condition): + netlist.reset() + with tempfile.TemporaryDirectory(dir=najaeda_test_path) as temp_dir: + source = os.path.join(temp_dir, "memory.sv") + with open(source, "w") as stream: + stream.write("""// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +module memory(input logic clk, rst, + input logic [1:0] addr, input logic [7:0] data, + output logic [7:0] q); + logic [7:0] mem_q [0:3]; + logic [7:0] mem_d [0:3]; + always_comb begin + mem_d = mem_q; + mem_d[addr] = data; + end + always_ff @(posedge clk) begin + if (CONDITION) mem_q <= '{8'h10, 8'h21, 8'h32, 8'h43}; + else mem_q <= mem_d; + end + assign q = mem_q[addr]; +endmodule +""".replace("CONDITION", condition)) + netlist.load_system_verilog(source) + top = naja.NLUniverse.get().getTopDesign() + memories = [inst.getModel() for inst in top.getInstances() + if inst.getModel().getName().startswith("naja_mem__")] + self.assertEqual(1, len(memories)) + reset = memories[0].getScalarTerm("RST") + self.assertIsNotNone(reset) + self.assertEqual(naja.SNLTermRole.SyncReset, reset.getRole()) + self.assertTrue(reset.is_reset()) + self.assertTrue(reset.is_sync_reset()) + self.assertFalse(reset.is_async_reset()) + self.assertEqual(level, reset.getResetActiveLevel()) + def test_system_verilog_config_diagnostics_default(self): config = netlist.SystemVerilogConfig() self.assertEqual( diff --git a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestMemoryInference.cpp b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestMemoryInference.cpp index 1d5cc13ca..e52582d67 100644 --- a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestMemoryInference.cpp +++ b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestMemoryInference.cpp @@ -1182,6 +1182,13 @@ endmodule } } ASSERT_NE(nullptr, memoryInst); + auto* reset = NLDB0::getMemoryReset(memoryInst->getModel()); + ASSERT_NE(nullptr, reset); + EXPECT_EQ(SNLDesignModeling::SNLTermRole::SyncReset, + SNLDesignModeling::getTermRole(reset)); + EXPECT_EQ(SNLDesignModeling::SNLActiveLevel::High, + SNLDesignModeling::getResetActiveLevel(reset)); + auto* rstEnableParam = memoryInst->getInstParameter(NLName("RST_ENABLE")); auto* rstAsyncParam = memoryInst->getInstParameter(NLName("RST_ASYNC")); @@ -1253,6 +1260,13 @@ endmodule } } ASSERT_NE(nullptr, memoryInst); + auto* reset = NLDB0::getMemoryReset(memoryInst->getModel()); + ASSERT_NE(nullptr, reset); + EXPECT_EQ(SNLDesignModeling::SNLTermRole::SyncReset, + SNLDesignModeling::getTermRole(reset)); + EXPECT_EQ(SNLDesignModeling::SNLActiveLevel::Low, + SNLDesignModeling::getResetActiveLevel(reset)); + auto* rstEnableParam = memoryInst->getInstParameter(NLName("RST_ENABLE")); auto* rstAsyncParam = memoryInst->getInstParameter(NLName("RST_ASYNC")); diff --git a/test/nl/snl/decorators/SNLDesignModelingTest0.cpp b/test/nl/snl/decorators/SNLDesignModelingTest0.cpp index f58a0a41e..05d95405e 100644 --- a/test/nl/snl/decorators/SNLDesignModelingTest0.cpp +++ b/test/nl/snl/decorators/SNLDesignModelingTest0.cpp @@ -555,6 +555,28 @@ TEST_F(SNLDesignModelingTest0, EXPECT_TRUE(SNLDesignModeling::isDataInput(inst->getInstTerm(wdata0))); EXPECT_TRUE(SNLDesignModeling::isDataOutput(inst->getInstTerm(rdata0))); + using Mode = SNLDesignModeling::MemoryResetMode; + using Role = SNLDesignModeling::SNLTermRole; + using Level = SNLDesignModeling::SNLActiveLevel; + struct ResetCase { Mode mode; Role role; Level level; }; + for (const auto& resetCase : { + ResetCase{Mode::None, Role::Other, Level::NA}, + ResetCase{Mode::AsyncLow, Role::AsyncReset, Level::Low}, + ResetCase{Mode::AsyncHigh, Role::AsyncReset, Level::High}, + ResetCase{Mode::SyncLow, Role::SyncReset, Level::Low}, + ResetCase{Mode::SyncHigh, Role::SyncReset, Level::High}}) { + SCOPED_TRACE(static_cast(resetCase.mode)); + interface.resetMode = resetCase.mode; + SNLDesignModeling::setMemoryInterface(mem, interface); + EXPECT_EQ(resetCase.role, SNLDesignModeling::getTermRole(rst)); + EXPECT_EQ(resetCase.level, SNLDesignModeling::getResetActiveLevel(rst)); + EXPECT_EQ(resetCase.role, SNLDesignModeling::getTermRole(inst->getInstTerm(rst))); + EXPECT_EQ(resetCase.level, + SNLDesignModeling::getResetActiveLevel(inst->getInstTerm(rst))); + EXPECT_EQ(resetCase.role != Role::Other, SNLDesignModeling::isReset(rst)); + EXPECT_EQ(resetCase.role == Role::SyncReset, SNLDesignModeling::isSyncReset(rst)); + } + auto plain = SNLDesign::create(prims, SNLDesign::Type::Primitive, NLName("PLAIN")); auto plainIn = SNLScalarTerm::create( plain, SNLTerm::Direction::Input, NLName("I")); diff --git a/test/nl/snl/kernel/NLDB0Test.cpp b/test/nl/snl/kernel/NLDB0Test.cpp index 19385c0f6..08cebeecb 100644 --- a/test/nl/snl/kernel/NLDB0Test.cpp +++ b/test/nl/snl/kernel/NLDB0Test.cpp @@ -1111,12 +1111,15 @@ TEST_F(NLDB0Test, testMemoryPrimitiveSyncResetModes) { NLUniverse::create(); ASSERT_NE(nullptr, NLUniverse::get()); + using Role = SNLDesignModeling::SNLTermRole; + using Level = SNLDesignModeling::SNLActiveLevel; auto checkMemory = [](NLDB0::MemoryResetMode mode, const char* expectedName, const char* expectedEnable, const char* expectedAsync, const char* expectedActiveLow, - SNLDesignModeling::MemoryResetMode expectedModelingMode) { + SNLDesignModeling::MemoryResetMode expectedModelingMode, + Role expectedRole, Level expectedLevel) { NLDB0::MemorySignature signature; signature.width = 4; signature.depth = 8; @@ -1131,6 +1134,12 @@ TEST_F(NLDB0Test, testMemoryPrimitiveSyncResetModes) { EXPECT_EQ(expectedEnable, memory->getParameter(NLName("RST_ENABLE"))->getValue()); EXPECT_EQ(expectedAsync, memory->getParameter(NLName("RST_ASYNC"))->getValue()); EXPECT_EQ(expectedActiveLow, memory->getParameter(NLName("RST_ACTIVE_LOW"))->getValue()); + auto* reset = NLDB0::getMemoryReset(memory); + ASSERT_NE(nullptr, reset); + EXPECT_EQ(expectedRole, SNLDesignModeling::getTermRole(reset)); + EXPECT_EQ(expectedLevel, SNLDesignModeling::getResetActiveLevel(reset)); + EXPECT_EQ(expectedRole != Role::Other, SNLDesignModeling::isReset(reset)); + EXPECT_EQ(expectedRole == Role::SyncReset, SNLDesignModeling::isSyncReset(reset)); EXPECT_EQ(expectedModelingMode, SNLDesignModeling::getMemoryInterface(memory).resetMode); }; @@ -1140,28 +1149,33 @@ TEST_F(NLDB0Test, testMemoryPrimitiveSyncResetModes) { "0", "0", "0", - SNLDesignModeling::MemoryResetMode::None); + SNLDesignModeling::MemoryResetMode::None, Role::Other, Level::NA); + checkMemory( + NLDB0::MemoryResetMode::AsyncLow, + "naja_mem__w4_d8_a3_r1_w1_rst_async_low", + "1", "1", "1", + SNLDesignModeling::MemoryResetMode::AsyncLow, Role::AsyncReset, Level::Low); checkMemory( NLDB0::MemoryResetMode::AsyncHigh, "naja_mem__w4_d8_a3_r1_w1_rst_async_high", "1", "1", "0", - SNLDesignModeling::MemoryResetMode::AsyncHigh); + SNLDesignModeling::MemoryResetMode::AsyncHigh, Role::AsyncReset, Level::High); checkMemory( NLDB0::MemoryResetMode::SyncLow, "naja_mem__w4_d8_a3_r1_w1_rst_sync_low", "1", "0", "1", - SNLDesignModeling::MemoryResetMode::SyncLow); + SNLDesignModeling::MemoryResetMode::SyncLow, Role::SyncReset, Level::Low); checkMemory( NLDB0::MemoryResetMode::SyncHigh, "naja_mem__w4_d8_a3_r1_w1_rst_sync_high", "1", "0", "0", - SNLDesignModeling::MemoryResetMode::SyncHigh); + SNLDesignModeling::MemoryResetMode::SyncHigh, Role::SyncReset, Level::High); } TEST_F(NLDB0Test, testLazyPrimitiveLibraryRecreationAndWidthErrors) { From 3b805536cea7884a7e9e9688a5b46e146416ac5c Mon Sep 17 00:00:00 2001 From: xtof Date: Mon, 28 Sep 2026 08:18:43 +0200 Subject: [PATCH 11/11] clean the docs --- src/najaeda/najaeda/docs/.readthedocs.yaml | 12 +++++-- src/najaeda/najaeda/docs/source/conf.py | 35 +++++++++++++++++++++ src/najaeda/najaeda/docs/source/raw_api.rst | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/najaeda/najaeda/docs/.readthedocs.yaml b/src/najaeda/najaeda/docs/.readthedocs.yaml index b737eef2c..36e9a8546 100644 --- a/src/najaeda/najaeda/docs/.readthedocs.yaml +++ b/src/najaeda/najaeda/docs/.readthedocs.yaml @@ -25,8 +25,14 @@ build: python: "3.12" jobs: post_install: - # Build this revision, including the extension used by autodoc. - - python -m pip install . --config-settings=cmake.define.PREGENERATED_PARSER_SOURCES=OFF --config-settings=build.tool-args=-j2 + # Autodoc needs this revision's extension, but not optimized machine code. + # Avoid spending the documentation build time limit optimizing the frontends. + - >- + python -m pip install . --verbose + --config-settings=cmake.define.PREGENERATED_PARSER_SOURCES=OFF + --config-settings=cmake.define.CMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG" + --config-settings=cmake.define.CMAKE_C_FLAGS_RELEASE="-O0 -DNDEBUG" + --config-settings=build.tool-args=-j2 submodules: include: all @@ -41,4 +47,4 @@ sphinx: # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: src/najaeda/najaeda/docs/requirements.txt \ No newline at end of file + - requirements: src/najaeda/najaeda/docs/requirements.txt diff --git a/src/najaeda/najaeda/docs/source/conf.py b/src/najaeda/najaeda/docs/source/conf.py index 332b0fb3f..27e0b3fb4 100644 --- a/src/najaeda/najaeda/docs/source/conf.py +++ b/src/najaeda/najaeda/docs/source/conf.py @@ -10,6 +10,9 @@ import re import sys +from docutils import nodes +from sphinx.errors import ExtensionError + # Prefer the installed package (including its compiled extension). Fall back # to the source package for local builds with the extension on PYTHONPATH. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) @@ -52,6 +55,38 @@ def read_naja_release(): ) from error sys.modules.setdefault("najaeda.naja", raw_naja) + +def check_raw_api_links(app, doctree, docname): + """Reject dangling raw API links, including targets removed by ``only``.""" + if docname != 'raw_api': + return + target_ids = { + target_id + for node in doctree.findall(nodes.Element) + for target_id in node.get('ids', []) + } + missing = { + node['refid'] + for node in doctree.findall(nodes.reference) + if node.get('refid', '').startswith('najaeda.naja.') + and node['refid'] not in target_ids + } + if missing: + raise ExtensionError( + 'Raw API reference targets are missing: ' + ', '.join(sorted(missing)) + ) + + +def check_raw_api_reference(app, env, node, contnode): + target = node.get('reftarget', '') + if target.startswith('najaeda.naja.'): + raise ExtensionError(f'Unresolved raw API reference: {target}') + + +def setup(app): + app.connect('doctree-resolved', check_raw_api_links) + app.connect('missing-reference', check_raw_api_reference) + templates_path = ['_templates'] exclude_patterns = [] diff --git a/src/najaeda/najaeda/docs/source/raw_api.rst b/src/najaeda/najaeda/docs/source/raw_api.rst index 42cfe81da..31ea7ecae 100644 --- a/src/najaeda/najaeda/docs/source/raw_api.rst +++ b/src/najaeda/najaeda/docs/source/raw_api.rst @@ -238,7 +238,7 @@ semantic source of truth. * - Object - Main public methods and values * - :class:`najaeda.naja.SNLParameter` - - ``create_decimal``, ``create_binary``, ``create_boolean``, ``create_string`` (optional default value), ``hasDefaultValue``, ``getValue``, ``getName``, ``getDesign``, ``destroy`` + - :meth:`create_decimal `, :meth:`create_binary `, :meth:`create_boolean `, :meth:`create_string ` (optional default value), :meth:`hasDefaultValue `, :meth:`getValue `, :meth:`getName `, :meth:`getDesign `, :meth:`destroy ` * - :class:`najaeda.naja.NLUniverse` - :meth:`create `, :meth:`destroy `, :meth:`get `, :meth:`getDB `, :meth:`getTopDB `, :meth:`setTopDB `, :meth:`getTopDesign `, :meth:`setTopDesign `, :meth:`getUserDBs `, :meth:`getSNLDesign `, :meth:`getObject `, :meth:`applyDLE `, :meth:`applyConstantPropagation `, :meth:`getMaxFanout `, :meth:`getMaxLogicLevel ` * - :class:`najaeda.naja.NLDB`