diff --git a/.github/workflows/pulp-sv-regress.yml b/.github/workflows/pulp-sv-regress.yml new file mode 100644 index 000000000..4cc3cee6e --- /dev/null +++ b/.github/workflows/pulp-sv-regress.yml @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +name: PULP SV Regress + +on: + pull_request: + branches: [main] + paths: + - 'src/nl/**' + - 'src/najaeda/**' + - 'thirdparty/slang' + - 'regress/sv/pulp_benchmarks.py' + - 'regress/sv/tests/test_pulp_benchmarks.py' + - '.github/workflows/pulp-sv-regress.yml' + push: + branches: [main] + workflow_dispatch: + inputs: + survey: + description: 'Survey all 23 variants (known failures make this job fail)' + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + pulp-sv: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -yq ccache libboost-dev libfl-dev capnproto libcapnp-dev libtbb-dev + - uses: ./.github/actions/setup-linux-release + with: + cache-scope: naja-module + - name: Configure canonical Release build + run: bash .github/scripts/configure-linux-release.sh + - name: Build najaeda Python extension + run: cmake --build build --target naja --parallel $(nproc) + - name: Prepare Python package + run: | + mkdir -p build/test/najaeda + cp -R src/najaeda/najaeda build/test/najaeda/ + ln -sf "${GITHUB_WORKSPACE}/build/src/nl/python/naja_wrapping/naja.so" build/test/najaeda/najaeda/naja.so + - name: Test benchmark runner + run: python3 -m unittest discover -s regress/sv/tests -p test_pulp_benchmarks.py + - name: Download pinned prepared RTL + run: | + curl -L --fail --retry 3 \ + https://github.com/pulp-platform/eda-benchmarks/releases/download/v0.1.0/pulp-benchmarks-v0.1.0.tar.gz \ + -o "${RUNNER_TEMP}/pulp-benchmarks-v0.1.0.tar.gz" + - name: Run PULP frontend regression + env: + SURVEY: ${{ inputs.survey }} + run: | + args=() + if [[ "${SURVEY}" == 'true' ]]; then args+=(--all); fi + python3 regress/sv/pulp_benchmarks.py \ + --archive "${RUNNER_TEMP}/pulp-benchmarks-v0.1.0.tar.gz" \ + --timeout 120 --lint-runner docker "${args[@]}" + - name: Run MemPool frontend elaboration regression + run: | + python3 regress/sv/pulp_benchmarks.py \ + --archive "${RUNNER_TEMP}/pulp-benchmarks-v0.1.0.tar.gz" \ + --case mempool.minpool --elaboration-only --timeout 240 + - name: Upload reports and generated netlists + if: always() + uses: actions/upload-artifact@v7 + with: + name: pulp-sv-regress + path: build/pulp-benchmarks + retention-days: 7 + - name: Show ccache statistics + if: always() + run: ccache --show-stats diff --git a/.github/workflows/sv-unit-test-sim.yml b/.github/workflows/sv-unit-test-sim.yml index be6a98fb9..8f86a4609 100644 --- a/.github/workflows/sv-unit-test-sim.yml +++ b/.github/workflows/sv-unit-test-sim.yml @@ -48,6 +48,8 @@ jobs: run: | python3 ${{ github.workspace }}/test/nl/formats/systemverilog/benchmarks/generate_najaeda_verilog.py + # streaming_lhs is tested as a Naja dump below: Verilator disagrees with + # Slang constant evaluation on partial streaming slices and target swaps. - name: Simulate SV benchmarks run: | docker run --rm \ @@ -57,6 +59,8 @@ jobs: -c 'set -euo pipefail cd /work/test/nl/formats/systemverilog/benchmarks for bench in \ + latch_loops \ + compound_shifts \ simple \ up_counter \ param_inst \ @@ -101,6 +105,9 @@ jobs: cd /work/test/nl/formats/systemverilog/benchmarks primitives_sv=../najaeda_primitives.v for bench in \ + latch_loops \ + compound_shifts \ + streaming_lhs \ simple \ up_counter \ param_inst \ diff --git a/regress/sv/PULP_RESULTS.md b/regress/sv/PULP_RESULTS.md new file mode 100644 index 000000000..a6a5a4add --- /dev/null +++ b/regress/sv/PULP_RESULTS.md @@ -0,0 +1,160 @@ + + +# PULP EDA benchmark frontend survey + +Measured on 2026-10-01 with Naja `927b9ca2` (0.7.27), rebuilt from source +using the existing macOS arm64 **Debug** CMake build and Python 3.14. The input is the +[prepared v0.1.0 release](https://github.com/pulp-platform/eda-benchmarks/releases/tag/v0.1.0), +SHA-256 `a27eaca9359cd2c0b646892325355e102ae563bd8f772c77f18697ff8c4ad603`. +This checksum also matches GitHub's published asset digest. + +Every variant uses its supplied top, filelist, macros, wrappers, and source +stubs. No RTL fixes, best-effort hierarchy, or unknown-module blackboxing were +used. The initial survey applied a 90-second wall-clock limit per load/dump. +Timings are local observations, not Linux Release CI performance guarantees; some +follow-up tests ran concurrently with the survey. + +## Recommended PR tier + +| Target | Input sources | Load/dump | Complete-dump lint | +| --- | ---: | ---: | ---: | +| `cv32e40p.default` | 28 | ~7–9 s | ~4 s, pass | +| `cvfpu.fp32_fma` | 8 | ~1.5 s | ~1 s, pass | +| `ibex.minimal` | 45 | ~4–5 s | ~2 s, pass | +| `spi_host.default` | 25 | ~1 s | <1 s, pass | + +These are the runner's default cases and the new `PULP SV Regress` workflow's +PR/push selection. FP32 FMA and SPI host add coverage beyond the existing +Ibex/CV32E40P external regressions. Those CPU cases also exercise this release's +specific source pins, configuration, and wrappers. + +Local lint used Verilator 5.052 and the complete Naja dump plus +`najaeda_primitives.v`, with only `ASCRANGE` suppressed, matching the existing +external-regression convention. CI uses the existing pinned Verilator 5.046 +Docker image. The Docker daemon was unavailable locally, so that image and the +Ubuntu GitHub job have not been executed here. No simulation or equivalence +check was performed; successful load/dump/lint does not prove functional +correctness. + +`ibex.full` also loads and dumps in ~20–22 seconds but fails strict lint with +two `UNOPTFLAT` warnings involving the secure/lockstep shadow core's ALU. It is +available as an explicit case, but not promoted to the green tier. These +warnings need investigation before either suppression or promotion. + +## Complete initial survey + +All 23 variants were attempted: **5 load/dump passes, 7 failures, 11 timeouts**. + +| Target | Load/dump outcome | Seconds | Observation | +| --- | --- | ---: | --- | +| `ara.default` | fail | 1.94 | Slang rejects implicit enum conversions in `lane_sequencer.sv`. | +| `cheshire.default` | fail | 2.68 | Slang rejects an enum conversion and use of `dtmcs_q` before declaration. | +| `cv32e40p.default` | pass | 6.52 | Also passes complete-dump lint; PR tier. | +| `cva6.default` | timeout | 90.08 | No unsupported error emitted before timeout; inconclusive. | +| `cvfpu.fp32_fma` | pass | 1.39 | Also passes complete-dump lint; PR tier. | +| `cvfpu.full` | timeout | 90.03 | No unsupported error emitted before timeout; inconclusive. | +| `ibex.minimal` | pass | 4.27 | Also passes complete-dump lint; PR tier. | +| `ibex.full` | pass | 20.15 | Load/dump passes; lint fails two `UNOPTFLAT` warnings. | +| `ita.default` | timeout | 90.02 | Before timeout: unsupported streaming assignment LHS, `ita_inp1_mux.sv:16`. | +| `mempool.minpool` | timeout | 90.12 | Before timeout: unsupported `always_latch` pattern, `register_file_1r_1w.sv:146`. | +| `mempool.mempool` | timeout | 90.09 | Before timeout: same unsupported latch pattern as minpool. | +| `mempool.terapool` | timeout | 90.10 | No unsupported error emitted before timeout; inconclusive. | +| `nvdla.top` | fail | 0.77 | Slang rejects inconsistent/missing timescales across modules. | +| `opope.default` | fail | 17.02 | Unsupported continuous-assignment LHS in `hci_core_fifo.sv:278`. | +| `pulp_c910.default` | fail | 4.04 | Slang reports expected-expression errors in `ct_lsu_sq.v:3860–3866`. | +| `redmule.default` | fail | 2.85 | Slang rejects implicit conversions in `redmule_streamer.sv:245,395`. | +| `serial_link.default` | fail | 2.02 | Unsupported register/interface aggregate types in `slink`/`slink_reg`, with cascading connection errors. | +| `snitch_cluster.minimal` | timeout | 90.04 | No unsupported error emitted before timeout; inconclusive. | +| `snitch_cluster.default` | timeout | 90.04 | No unsupported error emitted before timeout; inconclusive. | +| `snitch_cluster.default_latch` | timeout | 90.04 | Before timeout: unsupported latch pattern, `snitch_regfile_latch.sv:76`. | +| `snitch_cluster.occamy` | timeout | 90.05 | No unsupported error emitted before timeout; inconclusive. | +| `spatz.default` | timeout | 90.06 | Before timeout: unsupported compound shift assignment, `spatz_controller.sv:123`. | +| `spi_host.default` | pass | 0.97 | Also passes complete-dump lint; PR tier. | + +The initial local logs and summary are under `build/pulp-survey/`; the final +strict smoke/lint artifacts are under `build/pulp-benchmarks/`. Full-Ibex lint +artifacts are under `build/pulp-additional-passing/ibex.full/`. These generated +files are intentionally untracked. + +Useful follow-up work is streaming assignment LHS lowering (ITA), latch +statement patterns (MemPool/Snitch), compound shifts (Spatz), and aggregate/LHS +lowering (Serial Link/O-POPE). Re-run the inconclusive cases in a Release build +with a larger budget before deciding whether they belong in a scheduled tier. + +## Follow-up: compound shifts and streaming LHS + +The working-tree frontend changes add combinational compound shifts and +fixed-size streaming assignment LHS lowering. They preserve signed right +shifts, full-width shift counts, blocking-assignment replay, nested streams, +non-divisible slice widths, fixed arrays, multiple targets, and MSB-aligned +consumption of wider streaming sources. Dynamic/with-clause/overlapping targets +and sequential streaming assignments remain explicitly rejected. + +The generated `compound_shifts` benchmark passes 262,144 vectors, including a +70-bit shift count. Original RTL passes the same vectors. The generated +`streaming_lhs` benchmark passes 1,048,576 vectors. Verilator 5.052 disagrees on +partial streaming-target slices and swapping target variables when simulating +the original streaming RTL. A separate C++ constructor test forces Slang +constant evaluation through localparams and confirms the independent expected +partial-slice, nested-stream, wider-source, and swap values. The streaming CI +simulation therefore checks the Naja dump against these golden values, not +Verilator's streaming RTL behavior. + +Re-running the unmodified release with a 180-second limit: + +- **ITA:** the streaming-LHS diagnostic is gone. Loading finishes with only + unsupported latch patterns in `ita_register_file_1w_multi_port_read.sv:161` + (two elaborations) and `ita_register_file_1w_multi_port_read_we.sv:171`. +- **Spatz:** the compound-shift diagnostic is gone; the full cluster still + times out after 180 seconds in the local Debug build. This is not a full + Spatz pass. + +## Follow-up: independent always_latch writes + +The latch lowering now handles independent guarded writes in statement lists +and statically bounded nested loops. It preserves per-bit retention while an +enable is low, source-order priority for repeated writes, static array and +part-select targets, and both blocking and nonblocking scheduling when used +consistently. Dynamic targets, same-block read-after-write dependencies, +function calls, timed assignments, and mixed scheduling are rejected. + +The focused `latch_loops` fixture passes 16,385 state transitions in both the +original RTL and the generated Naja netlist. The complete frontend suite passes +1,140 tests. The ITA register-file modules, MemPool register file, and Snitch +register file now each load, dump, and pass Verilator lint, so these cases do +not require a new NLDB0 primitive. + +The MemPool full design now elaborates successfully with zero Naja unsupported +diagnostics. The `axi_dw_downsizer.sv` `resp_precedence` function-call RHS +cases and the `ctrl_registers.sv` loop-step lowering are handled by the +frontend. Structural dumping of the full MemPool netlist exceeded 300 seconds +locally, so CI runs this design as an elaboration-only frontend regression; +full dumping remains a separate performance investigation. + +The exhaustive case path accepts a no-`default` function case only when all +values of a small fixed-width domain are covered by constant labels. Other +incomplete cases remain unsupported. This keeps the lowering conservative while +supporting the AXI response precedence function. + +Logs are under `build/pulp-lowering-fixes/`. The initial survey table above is +retained as the baseline; neither design has been promoted to the passing CI +tier. + +## Reproduction and interpretation + +See [README.md](README.md#pulp-prepared-eda-benchmarks) for the commands and +workflow dispatch options. A complete survey intentionally returns nonzero +when any variant fails or times out. There are no expected-failure masks. + +A timeout alone is inconclusive. Some designs emit explicit unsupported +construct diagnostics before their timeout; those observations are useful +frontend work items even though the entire load did not finish. Strict Slang +compilation errors are separate from Naja lowering failures: upstream's Yosys +flow enables permissive compatibility options that this runner does not apply. + +The local code knowledge graph was refreshed incrementally for code. Its full +semantic refresh needs an unavailable LLM API key; the code refresh also +reported existing duplicate-node collisions in `SNLDesignModeling`. diff --git a/regress/sv/README.md b/regress/sv/README.md index c709747f3..551e45937 100644 --- a/regress/sv/README.md +++ b/regress/sv/README.md @@ -545,3 +545,63 @@ Remove the SV regress work directory: ```sh python3 regress/sv/sv_regress.py clean ``` + +## PULP prepared EDA benchmarks + +`pulp_benchmarks.py` complements the repository-checkout cases with the +[self-contained PULP release](https://github.com/pulp-platform/eda-benchmarks/releases/tag/v0.1.0). +It pins v0.1.0 and verifies the release archive's SHA-256 before extraction. +No RTL is vendored, generated, patched, or substituted. Each target uses the +release's `targets.mk` top and its variant-local `filelist.f`, including the +supplied wrappers, include directories, defines, and memory stubs. + +```sh +# Small PR tier: CV32E40P, FP32 FMA, minimal Ibex, and SPI host. +# Downloads the pinned release unless --archive supplies a local copy. +python3 regress/sv/pulp_benchmarks.py --lint-runner local + +# Complete, bounded survey; known failures deliberately return nonzero. +python3 regress/sv/pulp_benchmarks.py --all --timeout 90 + +# Individual cases and custom build/package locations are supported. +python3 regress/sv/pulp_benchmarks.py \ + --case ibex.full --timeout 120 \ + --archive /tmp/pulp-benchmarks-v0.1.0.tar.gz \ + --najaeda-path build/test/najaeda --output build/pulp-full-ibex +``` + +Use the Python interpreter matching the built Naja extension (Python 3.12+ +for archive extraction). The runner itself needs only the standard library; +the built `najaeda` package must also have its normal runtime dependencies. +`--lint-runner docker` uses the same Verilator v5.046 image as the external SV +runner; `local` requires Verilator on PATH. Omitting the option runs load/dump +only. Each load/dump and lint subprocess has a separate wall-clock limit. +Missing lint tools fail; they do not silently skip. + +The `PULP SV Regress` workflow builds Naja using the canonical Linux Release +configuration, runs the small tier and lints each complete generated netlist +with Naja's `najaeda_primitives.v` simulation models, copied into each case's +artifact directory. Both local and Docker lint use this copy; Docker mounts +the artifact directory once for both inputs. Manual dispatch with `survey=true` +attempts all 23 variants. It always uploads diagnostics, commands, counts, netlists, and +summaries. This is a frontend load/dump and syntax/linking regression, **not a +functional equivalence or simulation test**. No technology library is needed. + +Naja uses its ordinary strict loading behavior. The upstream Yosys flow's +`--compat-mode`, `--compat=vcs`, `--allow-use-before-declare`, +`--best-effort-hierarchy`, and variant-specific `yosys-slang.args` are not +silently forwarded or emulated. In particular, unknown-module blackboxing is +disabled. Naja unsupported diagnostics fail the run. Ordinary Slang warnings, +2-state comparison warnings, and memory-inference fallbacks remain in reports; +a fallback to generic sequential lowering is not itself an unsupported error. + +Artifacts default to `build/pulp-benchmarks/./` with an aggregate +`summary.json` in the parent directory. Each case has its own process, so a +crash, exception, or timeout does not prevent subsequent cases from running. +A timeout is an incomplete measurement, not proof of unsupported RTL. Stale +netlist/statistics files are removed before each case. Temporary extracted RTL +is removed afterward; the archive checksum and copied variant filelist make +source selection reproducible. + +The initial [23-variant survey and CI selection](PULP_RESULTS.md) records the +local build, timing limits, diagnostics, and remaining verification limits. diff --git a/regress/sv/pulp_benchmarks.py b/regress/sv/pulp_benchmarks.py new file mode 100644 index 000000000..7a64c6095 --- /dev/null +++ b/regress/sv/pulp_benchmarks.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +"""Load/dump the pinned, prepared PULP EDA benchmark release in isolated processes.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time +import urllib.request + +ROOT = Path(__file__).resolve().parents[2] +VERSION = "0.1.0" +PACKAGE = f"pulp-benchmarks-v{VERSION}" +URL = f"https://github.com/pulp-platform/eda-benchmarks/releases/download/v{VERSION}/{PACKAGE}.tar.gz" +SHA256 = "a27eaca9359cd2c0b646892325355e102ae563bd8f772c77f18697ff8c4ad603" +# Keep this small enough for pull requests; the complete survey is opt-in. +SMOKE = ("cv32e40p.default", "cvfpu.fp32_fma", "ibex.minimal", "spi_host.default") + + +def unpack(archive, destination): + if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256: + raise ValueError(f"SHA-256 mismatch for {archive}; expected {SHA256}") + with tarfile.open(archive) as source: + source.extractall(destination, filter="data") + return destination / PACKAGE + + +def targets(package): + pairs = re.findall(r"^([\w]+\.[\w]+)\.TOP := (\w+)\s*$", + (package / "targets.mk").read_text(), re.MULTILINE) + if not pairs or len(dict(pairs)) != len(pairs): + raise ValueError("Missing or duplicate benchmark tops in targets.mk") + return dict(pairs) + + +def validate_diagnostics(diagnostics): + if ("summary.status=success" not in diagnostics + or 'severity="error"' in diagnostics + or 'severity="fatal"' in diagnostics + or 'stage="naja_unsupported_' in diagnostics): + raise RuntimeError("Incomplete or unsupported lowering; see diagnostics.log") + + +def worker(top_name, artifacts, dump_netlist): + from najaeda import netlist + + start = time.monotonic() + top = netlist.load_system_verilog([], config=netlist.SystemVerilogConfig( + top=top_name, flist="filelist.f", + diagnostics_report_path=str(artifacts / "diagnostics.log"), + blackbox_unknown_modules=False, + )) + loaded = time.monotonic() + if top.get_name() != top_name or not top.count_terms(): + raise RuntimeError("Missing or unexpected top-level interface") + validate_diagnostics((artifacts / "diagnostics.log").read_text()) + stats = dict(top=top.get_name(), top_terms=top.count_terms(), + top_nets=top.count_nets(), top_instances=top.count_child_instances(), + load_seconds=loaded - start) + if dump_netlist: + top.dump_verilog(str(artifacts / "netlist.v")) + stats["dump_seconds"] = time.monotonic() - loaded + (artifacts / "design-stats.json").write_text(json.dumps(stats, indent=2) + "\n") + netlist.reset() + + +def run_target(package, name, top, artifacts, najaeda_path, timeout, lint_runner=None, + dump_netlist=True): + artifacts.mkdir(parents=True, exist_ok=True) + # Never let stale success products from an earlier run satisfy this run. + for filename in ("design-stats.json", "netlist.v", "najaeda_primitives.v", + "diagnostics.log", "lint.log", "summary.json"): + (artifacts / filename).unlink(missing_ok=True) + design, variant = name.split(".") + source_dir = package / design / "variants" / variant + source_list = (source_dir / "filelist.f").read_text() + (artifacts / "filelist.f").write_text(source_list) + command = [sys.executable, str(Path(__file__).resolve()), "--worker", top, + "--output", str(artifacts)] + if not dump_netlist: + command.append("--elaboration-only") + env = dict(os.environ, PYTHONPATH=str(najaeda_path)) + record = dict(target=name, top=top, command=command, cwd=str(source_dir), + source_count=sum(line.strip().endswith((".v", ".sv")) + for line in source_list.splitlines())) + start = time.monotonic() + with (artifacts / "load-dump.log").open("w") as log: + try: + result = subprocess.run(command, cwd=source_dir, env=env, stdout=log, + stderr=subprocess.STDOUT, timeout=timeout) + record["returncode"] = result.returncode + record["status"] = "passed" if result.returncode == 0 else "failed" + except subprocess.TimeoutExpired: + record["status"] = "timeout" + record["seconds"] = round(time.monotonic() - start, 3) + if record["status"] == "passed": + stats = artifacts / "design-stats.json" + dump = artifacts / "netlist.v" + if not stats.is_file() or (dump_netlist and (not dump.is_file() or dump.stat().st_size == 0)): + record["status"] = "failed" + record["error"] = "Missing load/dump products" + else: + record["design_stats"] = json.loads(stats.read_text()) + if dump_netlist: + record["dump_bytes"] = dump.stat().st_size + if record["status"] == "passed" and lint_runner and dump_netlist: + primitives = artifacts / "najaeda_primitives.v" + shutil.copyfile(ROOT / "test/nl/formats/systemverilog/benchmarks/najaeda_primitives.v", + primitives) + flags = ["--lint-only", "--sv", "--top-module", top, "-Wno-ASCRANGE"] + if lint_runner == "docker": + cidfile = artifacts / "lint-container.cid" + cidfile.unlink(missing_ok=True) + command = ["docker", "run", "--rm", "--cidfile", str(cidfile), "-v", f"{artifacts}:/work:ro", + "--entrypoint", "verilator", + "verilator/verilator:v5.046", *flags, "/work/netlist.v", "/work/najaeda_primitives.v"] + else: + command = ["verilator", *flags, str(artifacts / "netlist.v"), str(primitives)] + record["lint_command"] = command + start = time.monotonic() + with (artifacts / "lint.log").open("w") as log: + try: + result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, + timeout=timeout) + record["lint_returncode"] = result.returncode + if result.returncode: + record["status"] = "lint_failed" + except subprocess.TimeoutExpired: + record["status"] = "lint_timeout" + except OSError as error: + record["status"] = "lint_failed" + record["error"] = str(error) + finally: + if lint_runner == "docker" and cidfile.exists(): + try: + subprocess.run(["docker", "rm", "-f", cidfile.read_text().strip()], + stdout=log, stderr=subprocess.STDOUT, timeout=30) + except (OSError, subprocess.TimeoutExpired) as error: + record["status"] = "lint_failed" + record["error"] = f"Container cleanup failed: {error}" + record["lint_seconds"] = round(time.monotonic() - start, 3) + (artifacts / "summary.json").write_text(json.dumps(record, indent=2) + "\n") + return record + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--archive", type=Path, help="Use a local release archive (checksum still enforced)") + parser.add_argument("--output", type=Path, default=ROOT / "build/pulp-benchmarks") + parser.add_argument("--najaeda-path", type=Path, default=ROOT / "build/test/najaeda") + parser.add_argument("--case", action="append", help="Release target, repeatable; defaults to smoke selection") + parser.add_argument("--all", action="store_true", help="Survey every release target; failures remain failures") + parser.add_argument("--timeout", type=int, default=120, help="Per-target wall-clock limit in seconds") + parser.add_argument("--lint-runner", choices=("local", "docker"), + help="Also lint each complete dump with Naja primitive models") + parser.add_argument("--elaboration-only", action="store_true", + help="Load and validate the design without structural Verilog dumping") + parser.add_argument("--worker", help=argparse.SUPPRESS) + args = parser.parse_args() + args.output = args.output.resolve() + if args.worker: + worker(args.worker, args.output, not args.elaboration_only) + return 0 + if args.timeout <= 0 or (args.all and args.case) or (args.elaboration_only and args.lint_runner): + parser.error("--timeout must be positive; --all and --case are mutually exclusive") + args.output.mkdir(parents=True, exist_ok=True) + archive = args.archive or args.output / f"{PACKAGE}.tar.gz" + if not archive.exists() and args.archive is None: + with urllib.request.urlopen(URL, timeout=60) as response: + archive.write_bytes(response.read()) + with tempfile.TemporaryDirectory(prefix="pulp-rtl-", dir=args.output) as tmp: + package = unpack(archive, Path(tmp)) + available = targets(package) + selected = list(available) if args.all else list(dict.fromkeys(args.case or SMOKE)) + unknown = set(selected) - available.keys() + if unknown: + parser.error(f"Unknown cases: {sorted(unknown)}; available: {list(available)}") + summary = dict(release=VERSION, url=URL, sha256=SHA256, results=[]) + for name in selected: + result = run_target(package, name, available[name], args.output / name, + args.najaeda_path.resolve(), args.timeout, args.lint_runner, + not args.elaboration_only) + summary["results"].append(result) + (args.output / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print(f"{name}: {result['status']} ({result['seconds']:.2f}s)", flush=True) + return int(any(result["status"] != "passed" for result in summary["results"])) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/regress/sv/tests/test_pulp_benchmarks.py b/regress/sv/tests/test_pulp_benchmarks.py new file mode 100644 index 000000000..9ed40e2a9 --- /dev/null +++ b/regress/sv/tests/test_pulp_benchmarks.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pulp_benchmarks as pulp + + +class PulpBenchmarksTest(unittest.TestCase): + def test_unsupported_warnings_and_incomplete_diagnostics_fail(self): + success = "summary.status=success\n" + pulp.validate_diagnostics(success + 'diagnostic stage="naja_elaboration" severity="warning"') + for report in ("status=in_progress", success + 'severity="error"', + success + 'severity="fatal"', + success + 'stage="naja_unsupported_warning" severity="warning"', + success + 'stage="naja_unsupported_error" severity="warning"'): + with self.subTest(report=report), self.assertRaises(RuntimeError): + pulp.validate_diagnostics(report) + + def test_archive_checksum_is_required_before_extraction(self): + with tempfile.TemporaryDirectory() as tmp: + archive = Path(tmp) / "wrong.tar.gz" + archive.write_bytes(b"not the pinned release") + with self.assertRaisesRegex(ValueError, "SHA-256 mismatch"): + pulp.unpack(archive, Path(tmp) / "rtl") + self.assertFalse((Path(tmp) / "rtl").exists()) + + def test_target_inventory_rejects_duplicates_and_empty_input(self): + with tempfile.TemporaryDirectory() as tmp: + package = Path(tmp) + manifest = package / "targets.mk" + for content in ("", "x.y.TOP := top\nx.y.TOP := other\n"): + manifest.write_text(content) + with self.assertRaises(ValueError): + pulp.targets(package) + manifest.write_text("x.y.TOP := top\nz.w.TOP := other\n") + self.assertEqual({"x.y": "top", "z.w": "other"}, pulp.targets(package)) + + def test_failed_and_timed_out_workers_cannot_reuse_stale_outputs(self): + for outcome, expected in ((subprocess.CompletedProcess([], 1), "failed"), + (subprocess.TimeoutExpired([], 1), "timeout"), + (subprocess.CompletedProcess([], 0), "failed")): + with self.subTest(expected=expected), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "design/variants/default" + source.mkdir(parents=True) + (source / "filelist.f").write_text("input.sv\n") + artifacts = root / "artifacts" + artifacts.mkdir() + (artifacts / "netlist.v").write_text("stale") + (artifacts / "design-stats.json").write_text("{}") + with patch.object(pulp.subprocess, "run") as run: + if isinstance(outcome, Exception): + run.side_effect = outcome + else: + run.return_value = outcome + result = pulp.run_target(root, "design.default", "top", artifacts, root, 1) + self.assertEqual(expected, result["status"]) + self.assertFalse((artifacts / "netlist.v").exists()) + self.assertEqual(expected, json.loads((artifacts / "summary.json").read_text())["status"]) + self.assertEqual(1, run.call_args.kwargs["timeout"]) + self.assertEqual(source, run.call_args.kwargs["cwd"]) + + def test_lint_failure_is_not_a_load_dump_pass(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "design/variants/default" + source.mkdir(parents=True) + (source / "filelist.f").write_text("input.sv\n") + artifacts = root / "artifacts" + + def execute(command, **kwargs): + if command[0] == "verilator": + raise FileNotFoundError("verilator is missing") + (artifacts / "netlist.v").write_text("module top; endmodule\n") + (artifacts / "design-stats.json").write_text('{"top": "top"}') + return subprocess.CompletedProcess(command, 0) + + with patch.object(pulp.subprocess, "run", side_effect=execute): + result = pulp.run_target(root, "design.default", "top", artifacts, root, 1, "local") + self.assertEqual("lint_failed", result["status"]) + self.assertIn("verilator is missing", result["error"]) + + def test_lint_inputs_share_the_artifact_directory(self): + for runner in ("local", "docker"): + with self.subTest(runner=runner), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "design/variants/default" + source.mkdir(parents=True) + (source / "filelist.f").write_text("input.sv\n") + artifacts = root / "artifacts" + artifacts.mkdir() + primitives = artifacts / "najaeda_primitives.v" + primitives.write_text("stale") + + def execute(command, **kwargs): + if command[0] == sys.executable: + self.assertFalse(primitives.exists()) + (artifacts / "netlist.v").write_text("module top; endmodule\n") + (artifacts / "design-stats.json").write_text('{"top": "top"}') + else: + reference = pulp.ROOT / "test/nl/formats/systemverilog/benchmarks/najaeda_primitives.v" + self.assertEqual(reference.read_bytes(), primitives.read_bytes()) + inputs = command[command.index("-Wno-ASCRANGE") + 1:] + expected = ["netlist.v", "najaeda_primitives.v"] + if runner == "docker": + self.assertEqual(1, command.count("-v")) + self.assertIn(f"{artifacts}:/work:ro", command) + self.assertEqual([f"/work/{name}" for name in expected], inputs) + else: + self.assertEqual([str(artifacts / name) for name in expected], inputs) + return subprocess.CompletedProcess(command, 0) + + with patch.object(pulp.subprocess, "run", side_effect=execute): + result = pulp.run_target(root, "design.default", "top", artifacts, root, 1, runner) + self.assertEqual("passed", result["status"]) + + def test_elaboration_only_does_not_require_a_dump(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "design/variants/default" + source.mkdir(parents=True) + (source / "filelist.f").write_text("input.sv\n") + artifacts = root / "artifacts" + + def execute(command, **kwargs): + (artifacts / "design-stats.json").parent.mkdir(parents=True, exist_ok=True) + (artifacts / "design-stats.json").write_text('{"top": "top"}') + return subprocess.CompletedProcess(command, 0) + + with patch.object(pulp.subprocess, "run", side_effect=execute): + result = pulp.run_target(root, "design.default", "top", artifacts, root, 1, + dump_netlist=False) + self.assertEqual("passed", result["status"]) + self.assertNotIn("dump_bytes", result) + self.assertIn("--elaboration-only", result["command"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/core/NajaVersion.h.in b/src/core/NajaVersion.h.in index 0050d0947..f5a3887ea 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.26" }; +const std::string NAJA_VERSION { "0.7.27" }; 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) }; diff --git a/src/najaeda/najaeda/docs/source/loading.rst b/src/najaeda/najaeda/docs/source/loading.rst index c87b900fc..c2b902d08 100644 --- a/src/najaeda/najaeda/docs/source/loading.rst +++ b/src/najaeda/najaeda/docs/source/loading.rst @@ -62,6 +62,27 @@ the frontend's two-state matching behavior; overlapping items retain source-orde priority. A result must be defined for every selector value; functions with an uncovered path are rejected. +Combinational procedural lowering supports compound shifts (``<<=``, ``>>=``, +``<<<=``, ``>>>=``), preserving the full shift-count width and signed arithmetic +right-shift behavior. Blocking assignments earlier in the block are visible to +subsequent shifts. + +Fixed-size streaming assignment targets in ``always_comb`` support both stream +directions, explicit slice sizes (including partial slices), nested streams, +multiple targets, and fixed unpacked arrays. A wider source is consumed from its +most significant end. Dynamic target selections, target ``with`` clauses, +overlapping target bits, and sequential streaming assignments remain unsupported +and are rejected. + +``always_latch`` also supports independent guarded writes in statement lists and +statically bounded, nested ``for`` loops. Targets may be fixed array elements or +static bit/part selections. Each written bit retains its value while disabled; +later writes take priority, and unconditional writes become combinational logic. +This path supports blocking or nonblocking assignments, but rejects mixing them +on one variable, reading a variable written in the same block, dynamic targets, +function calls, expression side effects, and timed assignments. Existing simple +latch patterns retain their separate support. + By default, an incremental diagnostics report is written to ``naja_sv_diagnostics.log``. Set ``diagnostics_report_path=None`` in :class:`najaeda.netlist.SystemVerilogConfig` to disable that file and retain diff --git a/src/najaeda/najaeda/docs/source/raw_api.rst b/src/najaeda/najaeda/docs/source/raw_api.rst index ae45047b0..e28de1957 100644 --- a/src/najaeda/najaeda/docs/source/raw_api.rst +++ b/src/najaeda/najaeda/docs/source/raw_api.rst @@ -315,7 +315,13 @@ to disable the report file and keep diagnostics console-only. The same loader supports combinational ``casez``/``casex`` result-selection functions in continuous assignments, including escaped function names and -concatenated arguments. +concatenated arguments. Combinational compound shifts and fixed-size streaming +assignment targets are also supported, with the same supported forms and +explicit rejection of dynamic/overlapping streaming targets described in +:doc:`loading`. +Independent guarded ``always_latch`` writes also support static nested loops, +array elements, partial writes, and source-order priority, subject to the +dependency and scheduling restrictions in :doc:`loading`. The high-level ``netlist.load_system_verilog`` API exposes the same support; using the raw API is not required. See :doc:`loading` for matching semantics. diff --git a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp index 3fc47195d..05120ec70 100644 --- a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp +++ b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp @@ -4672,6 +4672,13 @@ endmodule if (!stripped) { return std::nullopt; // LCOV_EXCL_LINE } + if (stripped->kind == slang::ast::ExpressionKind::Streaming) { + const auto& stream = stripped->as(); + if (stream.isFixedSize() && stream.getBitstreamWidth() > 0) { + return static_cast(stream.getBitstreamWidth()); + } + return std::nullopt; + } const auto& canonical = stripped->type->getCanonicalType(); const auto bitWidth = canonical.getBitstreamWidth(); if (bitWidth <= 0) { @@ -13062,7 +13069,29 @@ endmodule return false; } if (!caseStmt.defaultCase && !mergedHasBits) { - return false; + const auto caseWidth = getIntegralExpressionBitWidth(caseStmt.expr); + if (!caseWidth || *caseWidth > 16) { + return false; + } + const size_t requiredValueCount = size_t{1} << *caseWidth; + std::unordered_set coveredValues; + for (const auto& item : caseStmt.items) { + for (const auto* expression : item.expressions) { + uint64_t value = 0; + if (!expression || !getConstantUnsigned(*expression, value)) { + return false; + } + coveredValues.insert(value); + } + } + if (coveredValues.size() != requiredValueCount) { + return false; + } + // All valid enum values are covered. SNL is two-state, so seed the + // impossible encoded-enum case with zero rather than inventing a + // four-state primitive; valid enum values select a real branch below. + mergedBits.assign(targetWidth, const0); + mergedHasBits = true; } // Wrap later alternatives first so the first matching source item wins, @@ -19544,6 +19573,15 @@ endmodule } } return; + case slang::ast::ExpressionKind::Streaming: + for (const auto& stream : + stripped->as().streams()) { + collectExpressionRootValueSymbols(*stream.operand, symbols); + if (stream.withExpr) { + collectExpressionRootValueSymbols(*stream.withExpr, symbols); + } + } + return; case slang::ast::ExpressionKind::Replication: { const auto& replication = stripped->as(); collectExpressionRootValueSymbols(replication.count(), symbols); @@ -19725,6 +19763,22 @@ endmodule // Save the raw LHS before stripping to root so we can collect index // symbols from selections like arr[idx]. const Expression* rawLhsExpr = lhsExpr; + if (lhsExpr && stripConversions(*lhsExpr)->kind == slang::ast::ExpressionKind::Streaming) { + std::vector leaves; + if (collectStreamingLHSLeaves(*lhsExpr, leaves)) { + for (const auto* leaf : leaves) { + const slang::ast::ValueSymbol* symbol = nullptr; + const auto* root = getTrackedAlwaysCombLHS(leaf); + if (!shouldIgnoreTrackedLHS(root, ignoredSymbols) && + tryGetRootValueSymbolReference(*root, symbol)) { + auto& deps = dependencyMap[symbol]; + deps.insert(conditionSymbols.begin(), conditionSymbols.end()); + collectExpressionRootValueSymbols(*action.rhs, deps); + } + } + } + return; + } lhsExpr = getTrackedAlwaysCombLHS(lhsExpr); if (shouldIgnoreTrackedLHS(lhsExpr, ignoredSymbols)) { return; @@ -20426,6 +20480,198 @@ endmodule return true; } + bool collectStreamingLHSLeaves( + const Expression& expr, + std::vector& leaves) const { + const auto* stripped = stripConversions(expr); + if (stripped->kind == slang::ast::ExpressionKind::Streaming) { + const auto& stream = stripped->as(); + if (!stream.isFixedSize()) { + return false; + } + for (const auto& item : stream.streams()) { + // Dynamic and with-clause targets need selector-aware writeback. + if (item.withExpr || !collectStreamingLHSLeaves(*item.operand, leaves)) { + return false; + } + } + return true; + } + if (stripped->kind == slang::ast::ExpressionKind::Concatenation) { + for (const auto* operand : stripped->as().operands()) { + if (!collectStreamingLHSLeaves(*operand, leaves)) { + return false; + } + } + return true; + } + if (hasDynamicSelectionInLHS(*stripped)) { + return false; + } + leaves.push_back(stripped); + return true; + } + + bool resolveStreamingLHSBits( + SNLDesign* design, + const Expression& expr, + std::vector& bits) { + const auto* stripped = stripConversions(expr); + bits.clear(); + if (stripped->kind == slang::ast::ExpressionKind::Streaming) { + const auto& stream = stripped->as(); + if (!stream.isFixedSize()) { + // Leaf collection rejects dynamically sized streams before bit resolution. + // LCOV_EXCL_START + return false; + // LCOV_EXCL_STOP + } + const auto streams = stream.streams(); + for (auto it = streams.rbegin(); it != streams.rend(); ++it) { + std::vector operandBits; + if (it->withExpr || !resolveStreamingLHSBits(design, *it->operand, operandBits)) { + return false; + } + bits.insert(bits.end(), operandBits.begin(), operandBits.end()); + } + if (bits.size() != stream.getBitstreamWidth()) { + // A fixed stream width is the sum of its validated operand widths. + // LCOV_EXCL_START + return false; + // LCOV_EXCL_STOP + } + // Map RHS bit positions to physical destinations. Packing the LHS + // order is the inverse of unpacking the RHS, including a short final + // slice when the total width is not a multiple of the slice size. + const auto slice = stream.getSliceSize(); + if (slice && slice < bits.size()) { + std::vector ordered; + size_t end = bits.size(); + while (end) { + const size_t begin = ((end - 1) / slice) * slice; + ordered.insert(ordered.end(), bits.begin() + begin, bits.begin() + end); + end = begin; + } + bits = std::move(ordered); + } + return !bits.empty(); + } + if (stripped->kind == slang::ast::ExpressionKind::Concatenation) { + const auto operands = stripped->as().operands(); + for (auto it = operands.rbegin(); it != operands.rend(); ++it) { + std::vector operandBits; + if (!resolveStreamingLHSBits(design, **it, operandBits)) { + return false; + } + bits.insert(bits.end(), operandBits.begin(), operandBits.end()); + } + return !bits.empty(); + } + return !hasDynamicSelectionInLHS(*stripped) && + resolveAssignmentLHSBits(design, *stripped, bits); + } + + bool applyCombinationalStreamingAssignment( + SNLDesign* design, + const AssignAction& action, + const Expression& trackedLhs, + const std::vector& lhsBits, + std::vector& dataBits, + const std::unordered_set* replaySymbols, + std::string& failureReason) { + std::vector leaves; + if (!collectStreamingLHSLeaves(*action.lhs, leaves)) { + // Assignment collection validates streaming leaves before replay. + // LCOV_EXCL_START + failureReason = "unsupported dynamic or with-clause streaming assignment LHS"; + return false; + // LCOV_EXCL_STOP + } + std::vector destinations; + std::unordered_map> roots; + { + const auto* savedLhs = activeProceduralReplayLHS_; + const auto* savedBits = activeProceduralReplayBits_; + auto* savedEnv = activeProceduralReplayEnv_; + activeProceduralReplayLHS_ = nullptr; + activeProceduralReplayBits_ = nullptr; + activeProceduralReplayEnv_ = nullptr; + const auto guard = slang::ScopeGuard([&]() { + activeProceduralReplayLHS_ = savedLhs; + activeProceduralReplayBits_ = savedBits; + activeProceduralReplayEnv_ = savedEnv; + }); + if (!resolveStreamingLHSBits(design, *action.lhs, destinations)) { + failureReason = "unable to resolve fixed streaming assignment LHS"; + return false; + } + for (const auto* leaf : leaves) { + const auto* root = getTrackedAlwaysCombLHS(leaf); + const slang::ast::ValueSymbol* symbol = nullptr; + if (!tryGetRootValueSymbolReference(*root, symbol) || + !resolveAssignmentLHSBits(design, *root, roots[symbol])) { + // Successfully resolved fixed leaves already have materialized storage roots. + // LCOV_EXCL_START + failureReason = "unable to resolve streaming assignment storage"; + return false; + // LCOV_EXCL_STOP + } + } + } + auto width = getExpressionBitstreamWidth(*action.rhs); + std::vector source; + const auto* savedLhs = activeProceduralReplayLHS_; + const auto* savedBits = activeProceduralReplayBits_; + activeProceduralReplayLHS_ = &trackedLhs; + activeProceduralReplayBits_ = &dataBits; + const auto guard = slang::ScopeGuard([&]() { + activeProceduralReplayLHS_ = savedLhs; + activeProceduralReplayBits_ = savedBits; + }); + if (!width || *width < destinations.size() || + !resolveExpressionBits(design, *action.rhs, *width, source) || source.size() != *width) { + failureReason = "unable to resolve streaming assignment RHS"; + return false; + } + // Streaming targets consume the most significant bits of a wider RHS. + const auto offset = source.size() - destinations.size(); + std::unordered_map replacements; + for (size_t i = 0; i < destinations.size(); ++i) { + if (!replacements.emplace(destinations[i], source[offset + i]).second) { + failureReason = "unsupported overlapping streaming assignment targets"; + return false; + } + } + const auto update = [&](const std::vector& physical, std::vector& values) { + for (size_t i = 0; i < physical.size(); ++i) { + if (auto found = replacements.find(physical[i]); found != replacements.end()) { + values[i] = found->second; + } + } + }; + update(lhsBits, dataBits); + if (activeProceduralReplayEnv_ && replaySymbols) { + for (const auto& [symbol, physical] : roots) { + if (!replaySymbols->contains(symbol)) { + continue; + } + auto [it, inserted] = activeProceduralReplayEnv_->try_emplace(symbol); + if (inserted) { + it->second = makeCombinationalInitialBits(design, physical); + } + if (it->second.size() != physical.size()) { + // Replay entries retain the width of their physical storage root. + // LCOV_EXCL_START + failureReason = "inconsistent streaming assignment replay width"; + return false; + // LCOV_EXCL_STOP + } + update(physical, it->second); + } + } + return true; + } + bool isForLoopControlSymbolRef(const Expression& expr, const Symbol& symbol) const { const auto* stripped = stripConversions(expr); if (!stripped || !slang::ast::ValueExpressionBase::isKind(stripped->kind)) { @@ -20757,8 +21003,17 @@ endmodule int64_t otherValue = 0; if (!getConstantInt64(leftIsLoopVar ? binaryExpr.right() : binaryExpr.left(), otherValue)) { - failureReason = "unsupported non-constant for-loop bound expression"; - return false; + uint64_t unsignedValue = 0; + const auto& boundExpr = leftIsLoopVar ? binaryExpr.right() : binaryExpr.left(); + if (!getConstantUnsigned(boundExpr, unsignedValue) || + unsignedValue > static_cast(std::numeric_limits::max())) { + failureReason = "unsupported non-constant for-loop bound expression"; + return false; + } + // getConstantInt64 already accepts unsigned constants that fit int64_t. + // LCOV_EXCL_START + otherValue = static_cast(unsignedValue); + // LCOV_EXCL_STOP } const int64_t lhsValue = leftIsLoopVar ? loopValue : otherValue; @@ -20804,25 +21059,36 @@ endmodule return true; } - if (getConstantInt64(rhsExpr, nextLoopValue)) { - return true; - } - const auto* strippedRHSExpr = stripConversions(rhsExpr); if (!strippedRHSExpr || strippedRHSExpr->kind != slang::ast::ExpressionKind::BinaryOp) { - return false; + return getConstantInt64(rhsExpr, nextLoopValue); } const auto& rhsBinaryExpr = strippedRHSExpr->as(); int64_t constantOperand = 0; + const auto getStepConstant = [&](const Expression& expression, int64_t& value) { + if (getConstantInt64(expression, value)) { + return true; + } + uint64_t unsignedValue = 0; + if (!getConstantUnsigned(expression, unsignedValue) || + unsignedValue > static_cast(std::numeric_limits::max())) { + return false; + } + // getConstantInt64 already accepts unsigned constants that fit int64_t. + // LCOV_EXCL_START + value = static_cast(unsignedValue); + return true; + // LCOV_EXCL_STOP + }; switch (rhsBinaryExpr.op) { case slang::ast::BinaryOperator::Add: if (isForLoopControlStepOperand( rhsBinaryExpr.left(), loopSymbol, allowLValueReferenceOperand) && - getConstantInt64(rhsBinaryExpr.right(), constantOperand)) { + getStepConstant(rhsBinaryExpr.right(), constantOperand)) { nextLoopValue = loopValue + constantOperand; return true; } @@ -20830,7 +21096,7 @@ endmodule rhsBinaryExpr.right(), loopSymbol, allowLValueReferenceOperand) && - getConstantInt64(rhsBinaryExpr.left(), constantOperand)) { + getStepConstant(rhsBinaryExpr.left(), constantOperand)) { nextLoopValue = constantOperand + loopValue; return true; } @@ -21001,13 +21267,14 @@ endmodule } int64_t nextLoopValue = 0; - if (evaluateForLoopStepRHS(assignExpr.right(), loopSymbol, loopValue, nextLoopValue)) { + if (evaluateForLoopStepRHS( + assignExpr.right(), loopSymbol, loopValue, nextLoopValue, true)) { loopValue = nextLoopValue; return true; } } - failureReason = "unsupported for-loop step expression"; + failureReason = "unsupported for-loop step expression: " + describeExpression(*strippedStepExpr); return false; } @@ -21074,7 +21341,12 @@ endmodule if (iterationCount++ >= kMaxForLoopUnrollIterations) { std::ostringstream reason; reason << "for-loop unroll iteration limit exceeded (" << kMaxForLoopUnrollIterations - << ")"; + << ", variable=" << std::string(loopSymbol->name) + << ", value=" << loopValue; + if (auto sourceInfo = getSourceInfo(forStmt.sourceRange)) { + reason << ", source_line=" << sourceInfo->line; + } + reason << ")"; failureReason = reason.str(); popBreakContext(); return false; @@ -22015,6 +22287,197 @@ endmodule return true; } + // Lower independent guarded writes, including statically unrolled array writes. + // Keep data and write enables separate so untouched bits retain their value + // through a latch rather than a combinational feedback mux. + bool lowerIndependentLatchWrites( + SNLDesign* design, + const Statement& stmt, + const std::optional& sourceRange, + std::string& failureReason) { + struct BitWrite { + SNLBitNet* output; + SNLBitNet* data; + SNLBitNet* enable; + }; + std::vector writes; + std::unordered_map writeIndices; + std::unordered_set readSymbols; + std::unordered_set writtenSymbols; + std::unordered_set loopVariables; + std::unordered_map scheduling; + auto* const0 = static_cast(getConstNet(design, false)); + auto* const1 = static_cast(getConstNet(design, true)); + auto mux = [&](SNLBitNet* select, SNLBitNet* low, SNLBitNet* high) -> SNLBitNet* { + if (select == const0 || low == high) { + return low; + } + if (select == const1) { + return high; + } + std::vector result; + if (!createMux2Instance(design, select, {low}, {high}, result, + sourceRange, nullptr, true) || result.size() != 1) { + // Canonical mux construction with validated one-bit nets cannot fail. + // LCOV_EXCL_START + return nullptr; + // LCOV_EXCL_STOP + } + return result.front(); + }; + auto collectReads = [&](const Expression& expression) { + bool supported = true; + auto visitor = slang::ast::makeVisitor( + [&](auto&, const slang::ast::ValueExpressionBase& value) { + readSymbols.insert(&value.symbol); + }, + [&](auto&, const slang::ast::CallExpression&) { supported = false; }, + [&](auto&, const slang::ast::AssignmentExpression&) { supported = false; }, + [&](auto& visitor, const slang::ast::UnaryExpression& unary) { + using Op = slang::ast::UnaryOperator; + if (unary.op == Op::Preincrement || unary.op == Op::Postincrement || + unary.op == Op::Predecrement || unary.op == Op::Postdecrement) { + supported = false; + } else { + visitor.visitDefault(unary); + } + }); + expression.visit(visitor); + if (!supported) { + failureReason = "calls and expression side effects in independent latch writes are unsupported"; + } + return supported; + }; + std::function visit; + visit = [&](const Statement& statement, SNLBitNet* guard) -> bool { + const auto* current = unwrapStatement(statement); + if (!current || guard == const0 || current->kind == slang::ast::StatementKind::Empty) { + return true; + } + if (current->kind == slang::ast::StatementKind::List) { + const auto& list = current->as().list; + for (const auto* item : list) { + const auto* child = item ? unwrapStatement(*item) : nullptr; + if (child && child->kind == slang::ast::StatementKind::ForLoop) { + for (const auto* variable : child->as().loopVars) { + loopVariables.insert(variable); + } + } + } + for (const auto* item : list) { + if (item && !visit(*item, guard)) { + return false; + } + } + return true; + } + if (current->kind == slang::ast::StatementKind::VariableDeclaration) { + const auto& variable = current->as().symbol; + if (loopVariables.contains(&variable)) { + return true; + } + } + if (current->kind == slang::ast::StatementKind::ForLoop) { + const auto& loop = current->as(); + return unrollForLoopStatement(loop, [&]() { return visit(loop.body, guard); }, + failureReason); + } + if (current->kind == slang::ast::StatementKind::Conditional) { + const auto& conditional = current->as(); + if (conditional.conditions.size() != 1 || !conditional.conditions[0].expr || + conditional.conditions[0].pattern) { + failureReason = "unsupported condition in independent latch writes"; + return false; + } + const auto& condition = *conditional.conditions[0].expr; + if (!collectReads(condition)) { + return false; + } + auto* select = resolveCombinationalConditionNet(design, condition, &failureReason); + if (!select) { + return false; + } + auto* trueGuard = mux(select, const0, guard); + auto* falseGuard = mux(select, guard, const0); + return trueGuard && falseGuard && visit(conditional.ifTrue, trueGuard) && + (!conditional.ifFalse || visit(*conditional.ifFalse, falseGuard)); + } + const Expression* lhs = nullptr; + AssignAction action; + if (!extractAssignment(*current, lhs, action) || !action.rhs || + assignmentActionRequiresCurrentBits(action)) { + failureReason = "unsupported statement in independent latch writes"; + return false; + } + const auto& assignment = current->as().expr; + if (assignment.as().timingControl) { + failureReason = "timed independent latch writes are unsupported"; + return false; + } + const slang::ast::ValueSymbol* root = nullptr; + if (!tryGetRootValueSymbolReference(*lhs, root) || hasDynamicSelectionInLHS(*lhs)) { + failureReason = "independent latch writes require statically selected targets"; + return false; + } + if (scheduling.contains(root) && scheduling.at(root) != action.scheduling) { + failureReason = "mixed blocking and nonblocking independent latch writes"; + return false; + } + scheduling[root] = action.scheduling; + writtenSymbols.insert(root); + if (!collectReads(*action.rhs)) { + return false; + } + std::vector lhsBits; + if (!resolveAssignmentLHSBits(design, *lhs, lhsBits, &failureReason, true) || + lhsBits.empty()) { + return false; + } + auto dataBits = buildAssignBits(design, action, resolveExpressionNet(design, *lhs), + lhsBits, nullptr, sourceRange); + if (dataBits.size() != lhsBits.size()) { + failureReason = "unable to resolve independent latch data"; + return false; + } + for (size_t bit = 0; bit < lhsBits.size(); ++bit) { + auto [it, inserted] = writeIndices.emplace(lhsBits[bit], writes.size()); + if (inserted) { + writes.push_back({lhsBits[bit], dataBits[bit], guard}); + } else { + auto& write = writes[it->second]; + write.data = mux(guard, write.data, dataBits[bit]); + write.enable = mux(guard, write.enable, const1); + if (!write.data || !write.enable) { + // Canonical mux construction with validated one-bit nets cannot fail. + // LCOV_EXCL_START + return false; + // LCOV_EXCL_STOP + } + } + } + return true; + }; + if (!visit(stmt, const1)) { + return false; + } + // This path deliberately excludes procedural read-after-write and feedback. + // Such blocks need scheduling replay, not independent write accumulation. + for (const auto* symbol : writtenSymbols) { + if (readSymbols.contains(symbol)) { + failureReason = "independent latch writes read a variable assigned in the same block"; + return false; + } + } + for (const auto& write : writes) { + if (write.enable == const1) { + createAssignInstance(design, write.data, write.output, sourceRange); + } else { + createDLatchInstance(design, write.enable, write.data, write.output, sourceRange); + } + } + return true; + } + bool extractAlwaysLatchPattern( const Statement& stmt, AlwaysLatchPattern& pattern) const { @@ -24084,6 +24547,21 @@ endmodule const Expression* lhsExpr = nullptr; AssignAction action; if (extractAssignment(*current, lhsExpr, action)) { + if (trackAlwaysCombDynamicLHS && lhsExpr && + stripConversions(*lhsExpr)->kind == slang::ast::ExpressionKind::Streaming) { + std::vector leaves; + if (!collectStreamingLHSLeaves(*lhsExpr, leaves)) { + setFailureReason("unsupported dynamic or with-clause streaming assignment LHS"); + return false; + } + for (const auto* leaf : leaves) { + const auto* root = getTrackedAlwaysCombLHS(leaf); + if (!shouldIgnoreTrackedLHS(root, ignoredSymbols)) { + appendTrackedSelectionLHS(root, lhsExpressions); + } + } + return true; + } if (trackAlwaysCombDynamicLHS) { lhsExpr = getTrackedAlwaysCombLHS(lhsExpr); } else if (trackSequentialFallbackLHS) { @@ -25435,9 +25913,68 @@ endmodule if (action.compoundOp) { if (!currentBits || currentBits->size() != targetWidth) { + // Compound actions are replayed with current bits at the target width. + // LCOV_EXCL_START failureReason = "unsupported compound assignment in always_comb without current LHS bits"; return false; + // LCOV_EXCL_STOP + } + + const auto op = *action.compoundOp; + const bool shiftLeft = op == slang::ast::BinaryOperator::LogicalShiftLeft || + op == slang::ast::BinaryOperator::ArithmeticShiftLeft; + if (shiftLeft || op == slang::ast::BinaryOperator::LogicalShiftRight || + op == slang::ast::BinaryOperator::ArithmeticShiftRight) { + // Slang represents a compound assignment as LValueReference op RHS. + // The count is self-determined: truncating it to the LHS width can + // turn an oversized shift into a small (or zero) shift. + const auto* rhs = stripConversions(*action.rhs); + if (!rhs || rhs->kind != slang::ast::ExpressionKind::BinaryOp || + rhs->as().op != op) { + // Slang represents compound shifts as the matching binary operation. + // LCOV_EXCL_START + failureReason = "unsupported compound shift expression"; + return false; + // LCOV_EXCL_STOP + } + const auto& count = rhs->as().right(); + auto countWidth = getIntegralExpressionBitWidth(count); + std::vector countBits; + if (!countWidth || !resolveShiftAmountBits( + design, count, *countWidth, countBits, &failureReason)) { + return false; + } + auto* const0 = static_cast(getConstNet(design, false)); + auto* const1 = static_cast(getConstNet(design, true)); + const bool signExtend = op == slang::ast::BinaryOperator::ArithmeticShiftRight && + action.lhs && action.lhs->type->isSigned(); + auto* fill = signExtend ? currentBits->back() : const0; + assignedBits = *currentBits; + for (size_t stage = 0; stage < countBits.size(); ++stage) { + if (countBits[stage] == const0) { + continue; + } + const auto distance = stage >= std::numeric_limits::digits + ? targetWidth : std::min(size_t {1} << stage, targetWidth); + std::vector nextBits; + nextBits.reserve(targetWidth); + for (size_t bit = 0; bit < targetWidth; ++bit) { + auto* shifted = fill; + if (shiftLeft ? bit >= distance : distance < targetWidth - bit) { + shifted = assignedBits[shiftLeft ? bit - distance : bit + distance]; + } + auto* result = shifted; + if (countBits[stage] != const1 && assignedBits[bit] != shifted) { + result = SNLScalarNet::create(design); + createMux2Instance(design, countBits[stage], assignedBits[bit], shifted, + result, getSourceRange(*action.rhs)); + } + nextBits.push_back(result); + } + assignedBits = std::move(nextBits); + } + return true; } const Expression* compoundRhsExpr = action.rhs; @@ -28178,6 +28715,14 @@ endmodule const Expression* assignedLHS = nullptr; AssignAction action; if (extractAssignment(*current, assignedLHS, action)) { + if (stripConversions(*assignedLHS)->kind == slang::ast::ExpressionKind::Streaming) { + if (preserveAssignmentScheduling || !assignmentUpdatesActiveProceduralValue(action)) { + failureReason = "unsupported sequential streaming assignment"; + return false; + } + return applyCombinationalStreamingAssignment( + design, action, lhsExpr, lhsBits, dataBits, replaySymbols, failureReason); + } const bool assignsCurrentLhs = assignedLHS && (sameLhs(assignedLHS, &lhsExpr) || @@ -28412,6 +28957,19 @@ endmodule if (!assignedExpr) { continue; // LCOV_EXCL_LINE } + if (stripConversions(*assignedExpr)->kind == slang::ast::ExpressionKind::Streaming) { + std::vector streamBits; + if (!resolveStreamingLHSBits(design, *assignedExpr, streamBits)) { + std::fill(assignedMask.begin(), assignedMask.end(), true); + return assignedMask; + } + for (auto* bit : streamBits) { + if (auto found = lhsBitOffsets.find(bit); found != lhsBitOffsets.end()) { + assignedMask[found->second] = true; + } + } + continue; + } const auto* trackedAssignedExpr = getTrackedAlwaysCombLHS(assignedExpr); const bool targetsTrackedLhs = sameLhs(assignedExpr, &lhsExpr) || @@ -31904,8 +32462,8 @@ endmodule std::string& latchFailureReason) -> bool { AlwaysLatchPattern pattern; if (!extractAlwaysLatchPattern(latchStmt, pattern)) { - latchFailureReason = "unsupported statement pattern for always_latch lowering"; - return false; + return lowerIndependentLatchWrites( + design, latchStmt, latchSourceRange, latchFailureReason); } auto* lhsNet = resolveExpressionNet(design, *pattern.lhs); diff --git a/test/nl/formats/systemverilog/benchmarks/Makefile b/test/nl/formats/systemverilog/benchmarks/Makefile index bf831b174..459149577 100644 --- a/test/nl/formats/systemverilog/benchmarks/Makefile +++ b/test/nl/formats/systemverilog/benchmarks/Makefile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -BENCHMARKS := simple up_counter param_inst binary_ops_supported byte_ports \ +BENCHMARKS := latch_loops compound_shifts streaming_lhs simple up_counter param_inst binary_ops_supported byte_ports \ implicit_width_ports port_directions non_ansi_ports model_reuse \ unary_not unary_not_binary_supported unary_not_arithmetic_binary_supported \ seq_add_out_plus_one seq_add_one_plus_out seq_enable_else_default \ diff --git a/test/nl/formats/systemverilog/benchmarks/compound_shifts/Makefile b/test/nl/formats/systemverilog/benchmarks/compound_shifts/Makefile new file mode 100644 index 000000000..7c44f111b --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/compound_shifts/Makefile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +TOP := compound_shifts_top +SV := compound_shifts.sv +TB := tb_compound_shifts.cpp + +include ../Makefile.verilator diff --git a/test/nl/formats/systemverilog/benchmarks/compound_shifts/compound_shifts.sv b/test/nl/formats/systemverilog/benchmarks/compound_shifts/compound_shifts.sv new file mode 100644 index 000000000..dfb4be947 --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/compound_shifts/compound_shifts.sv @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +module compound_shifts_top( + input logic [7:0] value, + input logic [9:0] amount, + output logic [7:0] left_o, right_o, arithmetic_left_o, unsigned_arithmetic_o, + output logic signed [7:0] signed_right_o, + output logic [7:0] replay_o, + output logic [3:0] field_o, + output logic [7:0] constant_o, giant_o +); + typedef struct packed { logic [3:0] high; logic [3:0] low; } pair_t; + pair_t pair; + logic [69:0] wide_amount; + always_comb begin + left_o = value; + left_o <<= amount; + right_o = value; + right_o >>= amount; + arithmetic_left_o = value; + arithmetic_left_o <<<= amount; + unsigned_arithmetic_o = value; + unsigned_arithmetic_o >>>= amount; + signed_right_o = value; + signed_right_o >>>= amount; + replay_o = value; + replay_o <<= 1; + replay_o >>= amount; + pair = value; + pair.low <<= amount; + field_o = pair.low; + wide_amount = {value[0], 59'b0, amount}; + giant_o = value; + giant_o <<= wide_amount; + constant_o = value; + constant_o >>= 2; + end +endmodule diff --git a/test/nl/formats/systemverilog/benchmarks/compound_shifts/tb_compound_shifts.cpp b/test/nl/formats/systemverilog/benchmarks/compound_shifts/tb_compound_shifts.cpp new file mode 100644 index 000000000..71ae3475b --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/compound_shifts/tb_compound_shifts.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +#include "Vcompound_shifts_top.h" +#include "verilated.h" +#include + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + Vcompound_shifts_top dut; + for (unsigned value = 0; value < 256; ++value) { + for (unsigned amount = 0; amount < 1024; ++amount) { + dut.value = value; + dut.amount = amount; + dut.eval(); + const unsigned left = amount < 8 ? (value << amount) & 255 : 0; + const unsigned right = amount < 8 ? value >> amount : 0; + const unsigned arithmetic = amount < 8 + ? ((value >> amount) | ((value & 128) ? (255 << (8 - amount)) : 0)) & 255 + : ((value & 128) ? 255 : 0); + const unsigned replay = amount < 8 ? ((value << 1) & 255) >> amount : 0; + const unsigned field = amount < 4 ? (value << amount) & 15 : 0; + if (dut.left_o != left || dut.arithmetic_left_o != left || + dut.right_o != right || dut.unsigned_arithmetic_o != right || + dut.signed_right_o != arithmetic || dut.replay_o != replay || + dut.giant_o != ((value & 1) ? 0 : left) || dut.field_o != field || dut.constant_o != (value >> 2)) { + std::printf("FAIL compound shifts value=%u amount=%u left=%u right=%u signed=%u replay=%u field=%u\n", + value, amount, dut.left_o, dut.right_o, dut.signed_right_o, dut.replay_o, dut.field_o); + return 1; + } + } + } + std::puts("compound_shifts: 262144 vectors passed"); + dut.final(); +} diff --git a/test/nl/formats/systemverilog/benchmarks/generate_najaeda_verilog.py b/test/nl/formats/systemverilog/benchmarks/generate_najaeda_verilog.py index 3425b4698..8bcb3a415 100644 --- a/test/nl/formats/systemverilog/benchmarks/generate_najaeda_verilog.py +++ b/test/nl/formats/systemverilog/benchmarks/generate_najaeda_verilog.py @@ -9,6 +9,9 @@ BENCHMARKS = ( + ("latch_loops/latch_loops.sv", "latch_loops/latch_loops_naja.v"), + ("compound_shifts/compound_shifts.sv", "compound_shifts/compound_shifts_naja.v"), + ("streaming_lhs/streaming_lhs.sv", "streaming_lhs/streaming_lhs_naja.v"), ("simple/simple.sv", "simple/simple_naja.v"), ("up_counter/up_counter.sv", "up_counter/up_counter_naja.v"), ("param_inst/param_inst.sv", "param_inst/param_inst_naja.v"), diff --git a/test/nl/formats/systemverilog/benchmarks/latch_loops/Makefile b/test/nl/formats/systemverilog/benchmarks/latch_loops/Makefile new file mode 100644 index 000000000..8091dae24 --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/latch_loops/Makefile @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +TOP := latch_loops_top +SV := latch_loops.sv +TB := tb_latch_loops.cpp +VERILATOR_FLAGS := -Wno-COMBDLY -Wno-NOLATCH + +include ../Makefile.verilator diff --git a/test/nl/formats/systemverilog/benchmarks/latch_loops/latch_loops.sv b/test/nl/formats/systemverilog/benchmarks/latch_loops/latch_loops.sv new file mode 100644 index 000000000..b3d0e03be --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/latch_loops/latch_loops.sv @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +module latch_loops_top( + input logic [3:0] en, + input logic [7:0] d, other, + input logic override_en, + output logic [31:0] array_o, nested_o, + output logic [7:0] priority_o, partial_o, nb_priority_o, branch_o, + output logic [3:0] zero_o +); + logic [7:0] words [0:3]; + logic [1:0][3:0] bytes [0:3]; + integer k; + always_latch begin + for (k = 0; k < 4; k++) + if (en[k]) words[k] <= d; + end + always_latch begin + zero_o = '0; + for (int i = 0; i < 4; i++) begin + for (int j = 0; j < 2; j++) begin + if (en[i]) bytes[i][j] = d[j*4 +: 4]; + end + end + end + always_latch begin + if (en[0]) priority_o = d; + if (override_en) priority_o = other; + if (en[1]) partial_o[3:0] = d[3:0]; + if (en[2]) partial_o[7:4] = d[7:4]; + if (en[3]) begin + if (override_en) partial_o[5:2] = other[3:0]; + else partial_o[5:2] = other[7:4]; + end + end + always_latch begin + if (en[0]) nb_priority_o <= d; + if (override_en) nb_priority_o <= other; + if (en[1]) branch_o[3:0] <= d[3:0]; + else if (en[2]) branch_o[7:4] <= other[7:4]; + end + for (genvar i = 0; i < 4; i++) begin + assign array_o[8*i +: 8] = words[i]; + assign nested_o[8*i +: 8] = bytes[i]; + end +endmodule diff --git a/test/nl/formats/systemverilog/benchmarks/latch_loops/tb_latch_loops.cpp b/test/nl/formats/systemverilog/benchmarks/latch_loops/tb_latch_loops.cpp new file mode 100644 index 000000000..45eb637d2 --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/latch_loops/tb_latch_loops.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +#include "Vlatch_loops_top.h" +#include "verilated.h" +#include +#include + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + Vlatch_loops_top dut; + uint32_t words = 0; + unsigned priority = 0, partial = 0, branch = 0; + auto check = [&](unsigned en, unsigned data, unsigned other, unsigned overrideEn) { + dut.en = en; + dut.d = data; + dut.other = other; + dut.override_en = overrideEn; + for (unsigned i = 0; i < 4; ++i) { + if (en & (1u << i)) words = (words & ~(255u << (8*i))) | (data << (8*i)); + } + if (en & 1) priority = data; + if (overrideEn) priority = other; + if (en & 2) partial = (partial & 240) | (data & 15); + if (en & 4) partial = (partial & 15) | (data & 240); + if (en & 8) partial = (partial & 195) | ((overrideEn ? other & 15 : other >> 4) << 2); + if (en & 2) branch = (branch & 240) | (data & 15); + else if (en & 4) branch = (branch & 15) | (other & 240); + dut.eval(); + if (dut.array_o != words || dut.nested_o != words || dut.priority_o != priority || + dut.partial_o != partial || dut.zero_o != 0 || + dut.nb_priority_o != priority || dut.branch_o != branch) { + std::printf("FAIL en=%u data=%u other=%u override=%u array=%x/%x nested=%x priority=%u/%u partial=%u/%u\n", + en, data, other, overrideEn, dut.array_o, words, dut.nested_o, + dut.priority_o, priority, dut.partial_o, partial); + return false; + } + return true; + }; + // Open every latch first; subsequent checks do not assume initial state. + dut.en = 4; + dut.d = 0; + dut.other = 0; + dut.override_en = 0; + dut.eval(); + if (!check(15, 0, 0, 1)) return 1; + for (unsigned data = 0; data < 256; ++data) { + for (unsigned en = 0; en < 16; ++en) { + for (unsigned overrideEn = 0; overrideEn < 2; ++overrideEn) { + unsigned other = (data * 73 + en * 11) & 255; + if (!check(en, data, other, overrideEn)) return 1; + if (!check(0, data ^ 255, other ^ 255, 0)) return 1; + } + } + } + std::puts("latch_loops: 16385 state transitions passed"); + dut.final(); +} diff --git a/test/nl/formats/systemverilog/benchmarks/streaming_lhs/Makefile b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/Makefile new file mode 100644 index 000000000..98beea74c --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/Makefile @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +TOP := streaming_lhs_top +# Verilator disagrees with Slang on partial LHS slices and target swaps. +# Simulate the Naja dump against the independently checked golden values. +# Generate with ../generate_najaeda_verilog.py first. +SV := streaming_lhs_naja.v ../najaeda_primitives.v +TB := tb_streaming_lhs.cpp + +include ../Makefile.verilator diff --git a/test/nl/formats/systemverilog/benchmarks/streaming_lhs/streaming_lhs.sv b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/streaming_lhs.sv new file mode 100644 index 000000000..c1574a22b --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/streaming_lhs.sv @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +module streaming_lhs_top( + input logic [19:0] value, + input logic enable, + output logic [9:0] forward_o, reverse_o, nested_o, selected_o, + output logic [7:0] swap_o, + output logic [15:0] array_o, + output logic [15:0] multi_o, stream_rhs_o +); + logic [3:0] a, b; + logic [3:0] upper; + logic [11:0] lower; + logic [7:0] bytes [0:1]; + logic [15:0] temp; + always_comb begin + forward_o = '0; + if (enable) {>>{forward_o}} = value; + {<<4{reverse_o}} = value[9:0]; + {<<4{{<<2{nested_o}}}} = value[9:0]; + selected_o = 10'h301; + {<<2{selected_o[7:2]}} = value[5:0]; + a = value[3:0]; + b = value[7:4]; + {>>{a,b}} = {b,a}; + swap_o = {a,b}; + {>>{bytes}} = {2{value[7:0]}}; + array_o = {bytes[0],bytes[1]}; + {>>{upper,lower}} = value[15:0]; + multi_o = {lower,upper}; + temp = value[15:0]; + temp ^= 16'h1234; + {>>{stream_rhs_o}} = {<<8{temp}}; + end +endmodule diff --git a/test/nl/formats/systemverilog/benchmarks/streaming_lhs/tb_streaming_lhs.cpp b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/tb_streaming_lhs.cpp new file mode 100644 index 000000000..16c287876 --- /dev/null +++ b/test/nl/formats/systemverilog/benchmarks/streaming_lhs/tb_streaming_lhs.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 The Naja authors +// SPDX-License-Identifier: Apache-2.0 +#include "Vstreaming_lhs_top.h" +#include "verilated.h" +#include + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + Vstreaming_lhs_top dut; + for (unsigned value = 0; value < (1u << 20); ++value) { + dut.value = value; + dut.enable = value & 1; + dut.eval(); + const unsigned reverse = ((value & 3) << 8) | (((value >> 2) & 15) << 4) | ((value >> 6) & 15); + const unsigned nested = ((reverse & 3) << 8) | (((reverse >> 2) & 3) << 6) | + (((reverse >> 4) & 3) << 4) | (((reverse >> 6) & 3) << 2) | ((reverse >> 8) & 3); + const unsigned selected = 0x301 | ((value & 3) << 6) | (((value >> 2) & 3) << 4) | + (((value >> 4) & 3) << 2); + const unsigned multi = ((value & 4095) << 4) | ((value >> 12) & 15); + const unsigned temp = (value ^ 0x1234) & 65535; + const unsigned rhs = ((temp & 255) << 8) | (temp >> 8); + if (dut.stream_rhs_o != rhs || dut.forward_o != ((value & 1) ? value >> 10 : 0) || dut.reverse_o != reverse || + dut.nested_o != nested || dut.selected_o != selected || dut.swap_o != (value & 255) || + dut.array_o != ((value & 255) * 257) || dut.multi_o != multi) { + std::printf("FAIL streaming value=%x fwd=%x rev=%x nested=%x selected=%x swap=%x array=%x multi=%x\n", + value, dut.forward_o, dut.reverse_o, dut.nested_o, dut.selected_o, dut.swap_o, dut.array_o, dut.multi_o); + return 1; + } + } + std::puts("streaming_lhs: 1048576 vectors passed"); + dut.final(); +} diff --git a/test/nl/formats/systemverilog/frontend/SNLSVConstructorCoverage.md b/test/nl/formats/systemverilog/frontend/SNLSVConstructorCoverage.md index af4597492..bbd5feeb5 100644 --- a/test/nl/formats/systemverilog/frontend/SNLSVConstructorCoverage.md +++ b/test/nl/formats/systemverilog/frontend/SNLSVConstructorCoverage.md @@ -53,6 +53,24 @@ are still expected in real designs: - `parseContinuousMuxDirectConstantsAndDestinations`: zero constants select the zero-net cache and reuse the same net across mux bits and instances. +The `SNLSVConstructorTestAlwaysComb.*Coverage` tests also cover: + +- Exhaustive return-function cases without a default, wide non-exhaustive cases, + and unsupported statements in a default branch. +- Nested concatenation streaming targets, checked bit by bit against their + source connections; dynamic storage, dynamic selections, unresolved storage, + unresolved RHS expressions, and dynamically sized RHS streams. +- Missing streaming storage through both the assigned-bit mask and replay + resolvers, including recursive concatenation failure propagation. +- Sequential streaming rejection during scheduling replay: a blocking write + followed by a read into another target forces this path. A separate test + checks the earlier combinational nonblocking-assignment diagnostic. +- Independent latch condition calls and side effects, real conditions and + targets, delayed nonblocking writes, unsupported data expressions, local + declarations, empty statements, and constant true/false guards. +- An unsigned loop-step parameter exceeding `INT64_MAX`, and a compound shift + whose count cannot be lowered. + ## Deliberate LCOV Exclusions The lines below are marked `LCOV_EXCL_LINE` because they require internal @@ -148,3 +166,18 @@ the current architecture: - Priority `always_latch` mux / OR construction and controlled-negedge clock inversion use validated nets and canonical DB0 primitives. Their null-result diagnostics require an internal primitive-construction failure. + +- Streaming LHS bit resolution: assignment collection has already rejected + dynamic streams and invalid leaves. Fixed widths must equal the sum of operand + widths; successfully resolved fixed leaves must have materialized roots, and + replay entries keep their root storage width. These invariant checks remain + defensive. Recursive operand-resolution failures are **tested**, not excluded. +- Loop-bound and step unsigned fallbacks: `getConstantInt64` already accepts + unsigned constants fitting `int64_t`. Success after that helper failed is + defensive; the unsigned-overflow rejection is tested with a typed parameter. +- Independent latch mux construction failures require failure of canonical DB0 + primitives with validated one-bit inputs. Constant guard shortcuts are tested. +- Compound assignment replay always supplies current bits at the target width; + Slang represents compound shifts with the matching binary operation. Missing + current bits and malformed compound-shift AST guards are excluded, while an + unresolvable shift count is tested. diff --git a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestAlwaysComb.cpp b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestAlwaysComb.cpp index 99ba9e383..fa22a1c72 100644 --- a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestAlwaysComb.cpp +++ b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestAlwaysComb.cpp @@ -4,6 +4,7 @@ #include "gtest/gtest.h" +#include #include #include #include @@ -2128,3 +2129,532 @@ endmodule svPath, {"unsupported always_comb assignment LHS: NamedValue base=s_n"}); } + +TEST_F(SNLSVConstructorTestAlwaysComb, parseCompoundShifts) { + SNLSVConstructor constructor(library_); + constructor.construct(std::filesystem::path(SNL_SV_BENCHMARKS_PATH) / + "compound_shifts/compound_shifts.sv"); + auto* top = library_->getSNLDesign(NLName("compound_shifts_top")); + ASSERT_NE(nullptr, top); + for (const auto* name : {"left_o", "right_o", "arithmetic_left_o", "unsigned_arithmetic_o", + "signed_right_o", "replay_o", "field_o", "constant_o", "giant_o"}) { + auto* output = top->getBusTerm(NLName(name)); + ASSERT_NE(nullptr, output); + for (auto* bit : output->getBusBits()) { + EXPECT_EQ(1u, countOutputInstTermDrivers(bit->getNet())) << name; + } + } +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseStreamingAssignmentLHS) { + SNLSVConstructor constructor(library_); + constructor.construct(std::filesystem::path(SNL_SV_BENCHMARKS_PATH) / + "streaming_lhs/streaming_lhs.sv"); + auto* top = library_->getSNLDesign(NLName("streaming_lhs_top")); + ASSERT_NE(nullptr, top); + for (const auto* name : {"forward_o", "reverse_o", "nested_o", "selected_o", + "swap_o", "array_o", "multi_o", "stream_rhs_o"}) { + auto* output = top->getBusTerm(NLName(name)); + ASSERT_NE(nullptr, output); + for (auto* bit : output->getBusBits()) { + EXPECT_EQ(1u, countOutputInstTermDrivers(bit->getNet())) << name; + } + } +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectDynamicStreamingAssignmentLHS) { + const auto path = createTestDirectory("dynamic_streaming_lhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [3:0] d, input logic [1:0] i, + output logic [7:0] q); + always_comb begin + q = '0; + {>>{q[i+:4]}} = d; + end +endmodule +)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"dynamic or with-clause streaming assignment LHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectOverlappingStreamingAssignmentLHS) { + const auto path = createTestDirectory("overlapping_streaming_lhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [15:0] d, output logic [7:0] q); + always_comb {>>{q,q}} = d; +endmodule +)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"overlapping streaming assignment targets"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectSequentialStreamingAssignmentLHS) { + const auto path = createTestDirectory("sequential_streaming_lhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic clk, input logic [7:0] d, + output logic [7:0] q); + always_ff @(posedge clk) {>>{q}} <= d; +endmodule +)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectWithClauseStreamingAssignmentLHS) { + const auto path = createTestDirectory("with_clause_streaming_lhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [15:0] d, + output logic [7:0] q [0:3]); + always_comb begin + q = '{default:'0}; + {>>{q with [1:2]}} = d; + end +endmodule +)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"with-clause streaming assignment LHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, streamingLHSGoldenValuesFromSlangConstantEvaluation) { + const auto path = createTestDirectory("streaming_lhs_constant_reference") / "top.sv"; + std::ofstream(path) << R"(module top(output wire [15:0][37:0] y); + function automatic logic [37:0] reference(input logic [19:0] value); + logic [9:0] reverse_value, nested_value, forward_value; + logic [3:0] a, b; + {<<4{reverse_value}} = value[9:0]; + {<<4{{<<2{nested_value}}}} = value[9:0]; + {>>{forward_value}} = value; + a = value[3:0]; + b = value[7:4]; + {>>{a,b}} = {b,a}; + return {forward_value,nested_value,reverse_value,a,b}; + endfunction + for (genvar i = 0; i < 16; i++) begin : g + // A localparam forces Slang evaluation, independently of Naja's procedural lowering. + localparam logic [37:0] EXPECTED = reference(20'(i * 20'h8421)); + assign y[i] = EXPECTED; + end +endmodule +)"; + SNLSVConstructor constructor(library_); + constructor.construct(path); + auto* top = library_->getSNLDesign(NLName("top")); + ASSERT_NE(nullptr, top); + auto* y = top->getBusTerm(NLName("y")); + ASSERT_NE(nullptr, y); + for (unsigned i = 0; i < 16; ++i) { + const uint64_t value = i * 0x8421; + const uint64_t reverse = ((value & 3) << 8) | (((value >> 2) & 15) << 4) | + ((value >> 6) & 15); + const uint64_t nested = ((reverse & 3) << 8) | (((reverse >> 2) & 3) << 6) | + (((reverse >> 4) & 3) << 4) | (((reverse >> 6) & 3) << 2) | ((reverse >> 8) & 3); + const uint64_t expected = ((value >> 10) << 28) | (nested << 18) | + (reverse << 8) | (value & 255); + for (unsigned bit = 0; bit < 38; ++bit) { + auto* net = getSingleAssignInputDriving(y->getBit(i * 38 + bit)->getNet()); + ASSERT_NE(nullptr, net); + EXPECT_TRUE((expected >> bit) & 1 ? net->isAssign1() : net->isAssign0()) + << "value=" << value << " bit=" << bit; + } + } +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseIndependentLatchLoops) { + SNLSVConstructor constructor(library_); + constructor.construct(std::filesystem::path(SNL_SV_BENCHMARKS_PATH) / + "latch_loops/latch_loops.sv"); + auto* top = library_->getSNLDesign(NLName("latch_loops_top")); + ASSERT_NE(nullptr, top); + for (const auto* name : {"array_o", "nested_o", "priority_o", "partial_o", "nb_priority_o", "branch_o", "zero_o"}) { + auto* output = top->getBusTerm(NLName(name)); + ASSERT_NE(nullptr, output); + for (auto* bit : output->getBusBits()) { + EXPECT_EQ(1u, countOutputInstTermDrivers(bit->getNet())) << name; + } + } +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectDependentLatchLoopWrites) { + const auto path = createTestDirectory("dependent_latch_loops") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic [7:0] d, + output logic [7:0] q, r); + always_latch begin + if (en) q = d; + if (en) r = q; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"read a variable assigned in the same block"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectMixedLatchLoopScheduling) { + const auto path = createTestDirectory("mixed_latch_loops") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic [7:0] d, + output logic [7:0] q); + always_latch begin + if (en) q = d; + if (!en) q <= d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"mixed blocking and nonblocking"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectLatchLoopFunctionDependencies) { + const auto path = createTestDirectory("latch_loop_function") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic [7:0] d, + output logic [7:0] q, r); + function automatic logic [7:0] read_q(); return q; endfunction + always_latch begin + if (en) q = d; + if (en) r = read_q(); + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"calls and expression side effects"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectDynamicLatchLoopTarget) { + const auto path = createTestDirectory("dynamic_latch_loop") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, d, input logic [2:0] index, + output logic [7:0] q, output logic r); + always_latch begin + if (en) q[index] = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"statically selected targets"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectLatchLoopConditionDependency) { + const auto path = createTestDirectory("latch_loop_condition") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, d, output logic q, r); + always_latch begin + if (en) q = d; + if (q) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"read a variable assigned in the same block"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchConditionCallCoverage) { + const auto path = createTestDirectory("reject_independent_latch_condition_call") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + function automatic logic f(input logic a); return a; endfunction + always_latch begin + if (f(en)) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"calls and expression side effects"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchConditionIncrementCoverage) { + const auto path = createTestDirectory("reject_independent_latch_condition_increment") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if (++d) q = en; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"calls and expression side effects"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchConditionAssignmentCoverage) { + const auto path = createTestDirectory("reject_independent_latch_condition_assignment") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if ((d = en)) q = en; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"calls and expression side effects"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchTimedWriteCoverage) { + const auto path = createTestDirectory("reject_independent_latch_timed_write") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if (en) q <= #1 d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"timed independent latch writes"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchRealConditionCoverage) { + const auto path = createTestDirectory("reject_independent_latch_real_condition") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, input real condition, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if (condition) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"Unsupported latch block"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchUnresolvedDataCoverage) { + const auto path = createTestDirectory("reject_independent_latch_unresolved_data") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output logic [1:0] q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if (en) q = d ** en; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve independent latch data"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchRealTargetCoverage) { + const auto path = createTestDirectory("reject_independent_latch_real_target") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, input logic d_in, + output real q, output logic r); + logic d; + assign d = d_in; + always_latch begin + if (en) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"Unsupported latch block"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectReturnFunctionWideCaseWithoutDefaultCoverage) { + const auto path = createTestDirectory("reject_return_function_wide_case_without_default") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [16:0] a, output logic q); + function automatic logic f(input logic [16:0] a); + case (a) + 17'd0: return 1; + endcase + endfunction + always_comb q = f(a); + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve always_comb RHS bits for Call"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectReturnFunctionUnsupportedDefaultCoverage) { + const auto path = createTestDirectory("reject_return_function_unsupported_default") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [16:0] a, output logic q); + function automatic logic f(input logic [16:0] a); + case (a) + 0: return 1; + default: while (a) return 0; + endcase + endfunction + always_comb q = f(a); + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve always_comb RHS bits for Call"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingUnresolvedRHSCoverage) { + const auto path = createTestDirectory("reject_streaming_unresolved_rhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a, output logic [7:0] q); + function automatic logic [7:0] f(input logic [7:0] a); + while (a) return a; + return 0; + endfunction + always_comb begin + {>>{q}} = f(a); + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve streaming assignment RHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectCompoundShiftUnresolvedCountCoverage) { + const auto path = createTestDirectory("reject_compound_shift_unresolved_count") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a, output logic [7:0] q); + function automatic logic [7:0] f(input logic [7:0] a); + while (a) return a; + return 0; + endfunction + always_comb begin + q = a; + q <<= f(a); + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"failed to resolve shift amount bits"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingNonblockingAssignmentCoverage) { + const auto path = createTestDirectory("reject_streaming_nonblocking_assignment") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a, output logic [7:0] q); + always_comb begin + {>>{q}} <= a; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, + {"scheduling-sensitive combinational non-blocking assignment has an ambiguous target"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingMissingStorageCoverage) { + const auto path = createTestDirectory("reject_streaming_missing_storage") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [15:0] d, output logic [7:0] q); + byte missing[]; + always_comb {>>{{q, missing[0]}}} = d; + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve fixed streaming assignment LHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingDynamicConcatenationLeafCoverage) { + const auto path = createTestDirectory("reject_streaming_dynamic_concatenation_leaf") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a, output logic [7:0] q); + always_comb begin + {>>{{q[a[0]],q[1]}}} = a; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"dynamic or with-clause streaming assignment LHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseExhaustiveReturnFunctionCaseCoverage) { + const auto path = createTestDirectory("parse_exhaustive_return_function_case") / "top.sv"; + std::ofstream(path) << R"(module top(input logic a, output logic q); + function automatic logic f(input logic a); + case (a) + 0: return 1'b0; + 1: return 1'b1; + endcase + endfunction + always_comb q = f(a); + endmodule)"; + SNLSVConstructor constructor(library_); + constructor.construct(path); + auto* top = library_->getSNLDesign(NLName("top")); + ASSERT_NE(nullptr, top); + EXPECT_EQ(1u, countOutputInstTermDrivers(top->getScalarTerm(NLName("q"))->getNet())); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseStreamingConcatenationBitConnectionsCoverage) { + const auto path = createTestDirectory("parse_streaming_concatenation_bit_connections") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a, output logic [7:0] q); + always_comb {>>{{q[7:4], q[3:0]}}} = a; + endmodule)"; + SNLSVConstructor constructor(library_); + constructor.construct(path); + auto* top = library_->getSNLDesign(NLName("top")); + ASSERT_NE(nullptr, top); + auto* q = top->getBusTerm(NLName("q")); + auto* a = top->getBusTerm(NLName("a")); + ASSERT_NE(nullptr, q); + ASSERT_NE(nullptr, a); + for (int bit = 0; bit < 8; ++bit) { + EXPECT_EQ(a->getBit(bit)->getNet(), getSingleAssignInputDriving(q->getBit(bit)->getNet())); + } +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingDynamicRHSCoverage) { + const auto path = createTestDirectory("reject_streaming_dynamic_rhs") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a[0:3], input logic [1:0] n, output logic [7:0] q); + always_comb {>>{q}} = {>>{a with [0+:n]}}; + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unable to resolve streaming assignment RHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectStreamingDynamicStorageCoverage) { + const auto path = createTestDirectory("reject_streaming_dynamic_storage") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [7:0] a); + byte q[]; + always_comb {>>{q}} = a; + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"dynamic or with-clause streaming assignment LHS"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectForLoopUnsignedStepOverflowCoverage) { + const auto path = createTestDirectory("reject_for_loop_unsigned_step_overflow") / "top.sv"; + std::ofstream(path) << R"(module top(input logic [3:0] d, output logic [3:0] q); + localparam logic [63:0] STEP = 64'h8000000000000000; + always_comb for (longint unsigned i = 0; i < 4; i = i + STEP) q[i] = d[i]; + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unsupported for-loop step expression"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectIndependentLatchLocalDeclarationCoverage) { + const auto path = createTestDirectory("reject_independent_latch_local_declaration") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, d, output logic q, r); + always_latch begin + automatic logic tmp; + if (en) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unsupported statement in independent latch writes"}); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseIndependentLatchConstantGuardCoverage) { + const auto path = createTestDirectory("parse_independent_latch_constant_guard") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, d, output logic q, r); + always_latch begin + if (1) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + constructor.construct(path); + auto* top = library_->getSNLDesign(NLName("top")); + ASSERT_NE(nullptr, top); + EXPECT_EQ(top->getScalarTerm(NLName("d"))->getNet(), + getSingleAssignInputDriving(top->getScalarTerm(NLName("q"))->getNet())); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, parseIndependentLatchEmptyAndFalseGuardCoverage) { + const auto path = createTestDirectory("parse_independent_latch_empty_and_false_guard") / "top.sv"; + std::ofstream(path) << R"(module top(input logic en, d, output logic q, r); + always_latch begin + ; + if (0) q = d; + if (en) r = d; + end + endmodule)"; + SNLSVConstructor constructor(library_); + constructor.construct(path); + auto* top = library_->getSNLDesign(NLName("top")); + ASSERT_NE(nullptr, top); + EXPECT_EQ(0u, countOutputInstTermDrivers(top->getScalarTerm(NLName("q"))->getNet())); + EXPECT_EQ(1u, countOutputInstTermDrivers(top->getScalarTerm(NLName("r"))->getNet())); +} + +TEST_F(SNLSVConstructorTestAlwaysComb, rejectSequentialStreamingSchedulingReplayCoverage) { + const auto path = createTestDirectory("reject_sequential_streaming_scheduling_replay") / "top.sv"; + std::ofstream(path) << R"(module top(input logic clk, input logic [7:0] d, output logic [7:0] q, r); + always_ff @(posedge clk) begin + q = d; + {>>{q}} = d; + // Reading q into another target forces scheduling replay. + r = q; + end + endmodule)"; + SNLSVConstructor constructor(library_); + expectUnsupportedConstruct(constructor, path, {"unsupported sequential streaming assignment"}); +} diff --git a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp index bbb057ccc..9919e8619 100644 --- a/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp +++ b/test/nl/formats/systemverilog/frontend/SNLSVConstructorTestSimple.cpp @@ -34878,11 +34878,11 @@ endmodule } TEST_F(SNLSVConstructorTestSimple, - parseAlwaysLatchPriorityIntermediateDifferentLHSUnsupported) { + parseAlwaysLatchPriorityIntermediateDifferentLHSSupported) { SNLSVConstructor constructor(library_); const auto svPath = writeSVTestFile( - "always_latch_priority_intermediate_different_lhs_unsupported", - R"(module always_latch_priority_intermediate_different_lhs_unsupported( + "always_latch_priority_intermediate_different_lhs_supported", + R"(module always_latch_priority_intermediate_different_lhs_supported( input logic en0_i, input logic en1_i, input logic d_i, @@ -34898,12 +34898,11 @@ TEST_F(SNLSVConstructorTestSimple, endmodule )"); - expectUnsupportedConstruct( - constructor, - svPath, - {"Unsupported latch block in module " - "'always_latch_priority_intermediate_different_lhs_unsupported'", - "unsupported statement pattern for always_latch lowering"}); + constructor.construct(svPath); + auto* top = library_->getSNLDesign( + NLName("always_latch_priority_intermediate_different_lhs_supported")); + ASSERT_NE(nullptr, top); + EXPECT_EQ(2u, countPrimitiveInstances(top, NLDB0::isDLatch)); } TEST_F(SNLSVConstructorTestSimple, @@ -34931,7 +34930,7 @@ endmodule svPath, {"Unsupported latch block in module " "'always_latch_priority_pattern_conjunction_unsupported'", - "unsupported statement pattern for always_latch lowering"}); + "unsupported condition in independent latch writes"}); } TEST_F(SNLSVConstructorTestSimple,