diff --git a/.github/actions/artifact-cache/action.yml b/.github/actions/artifact-cache/action.yml new file mode 100644 index 000000000..0fcc4448b --- /dev/null +++ b/.github/actions/artifact-cache/action.yml @@ -0,0 +1,165 @@ +--- +# DESCRIPTION: Github actions composite action +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +name: Artifact cache +description: >- + Cache a directory across workflow runs using a GitHub Actions artifact instead + of actions/cache (useful where artifacts are preferable, e.g. no 10 GB limit + on public repos). The base 'key' is scoped automatically: per pull request + (-pr-) or per branch (-branch-), and on a PR restore falls + back to the target branch's cache. Call once with mode=restore before the work + and once with mode=save after, passing the same 'key' and 'path'. + + Branch-scoped restores are provenance-checked (the artifact must come from a + same-repo push to that branch) - artifact names are a global, unauthenticated + namespace, so otherwise a fork PR could forge one. + + mode=restore calls the GitHub API via 'gh', so the caller's job must grant + 'actions: read'. + +inputs: + mode: + description: "'restore' or 'save'" + required: true + path: + description: "Directory to cache (archived on save, extracted into on restore)" + required: true + key: + description: "Base artifact name; scoped per PR/branch automatically" + required: true + retention-days: + description: "Artifact retention in days (mode=save)" + required: false + default: '3' + token: + description: "Token for the GitHub API (mode=restore); defaults to the job's GITHUB_TOKEN" + required: false + default: '' + +outputs: + key: + description: "Echoes the input 'key', so a later save can reuse it without repeating the expression" + value: ${{ inputs.key }} + cache-hit: + description: "'true' if an artifact was restored (mode=restore)" + value: ${{ steps.restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Check mode + shell: bash + env: + MODE: ${{ inputs.mode }} + run: | + case "$MODE" in + restore|save) ;; + *) echo "::error::artifact-cache: 'mode' must be 'restore' or 'save' (got '$MODE')"; exit 1 ;; + esac + + # Scope the base key: -pr- for pull requests (with the target branch + # as fallback), else -branch-. Refs come via env, not ${{ }} + # interpolation, so a branch name with shell metacharacters can't inject. + - name: Compute artifact name + id: name + shell: bash + env: + BASE_KEY: ${{ inputs.key }} + EVENT_NAME: ${{ github.event_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + sanitize() { printf '%s' "$1" | tr '/' '-'; } # artifact names cannot contain '/' + if [ "$EVENT_NAME" = pull_request ]; then + { + echo "primary=${BASE_KEY}-pr-${PR_NUMBER}" + echo "primary-branch=" + echo "fallback=${BASE_KEY}-branch-$(sanitize "$BASE_REF")" + echo "fallback-branch=$BASE_REF" + } >> "$GITHUB_OUTPUT" + else + { + echo "primary=${BASE_KEY}-branch-$(sanitize "$REF_NAME")" + echo "primary-branch=$REF_NAME" + echo "fallback=" + echo "fallback-branch=" + } >> "$GITHUB_OUTPUT" + fi + + - name: Restore artifact cache + id: restore + if: ${{ inputs.mode == 'restore' }} + continue-on-error: true # a cold start is fine; a restore failure must not fail the caller + shell: bash + env: + GH_TOKEN: ${{ inputs.token || github.token }} + CACHE_PATH: ${{ inputs.path }} + PRIMARY: ${{ steps.name.outputs.primary }} + PRIMARY_BRANCH: ${{ steps.name.outputs.primary-branch }} + FALLBACK: ${{ steps.name.outputs.fallback }} + FALLBACK_BRANCH: ${{ steps.name.outputs.fallback-branch }} + run: | + set -euo pipefail + mkdir -p "$CACHE_PATH" + # Newest non-expired artifact named $1 (empty if none). With a non-empty + # $2 (branch), require provenance: a same-repo run (head_repository_id == + # repository_id) whose head branch is $2, so a fork cannot forge it. + newest_run_id() { + want="$1" br="$2" gh api \ + "/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$1&per_page=100" \ + --jq '.artifacts[] + | select(.expired == false and .name == env.want) + | select(env.br == "" + or (.workflow_run.head_branch == env.br + and .workflow_run.head_repository_id == .workflow_run.repository_id)) + | [.created_at, (.workflow_run.id | tostring)] | @tsv' \ + | sort | tail -n1 | cut -f2 + } + restore() { # $1=name $2=branch(empty ok); returns 0 on a successful restore + local name="$1" br="$2" rid tarball + [ -n "$name" ] || return 1 + rid="$(newest_run_id "$name" "$br")" || return 1 + [ -n "$rid" ] || return 1 + echo "Restoring '$CACHE_PATH' from artifact '$name' (run $rid)" + gh run download "$rid" -R "$GITHUB_REPOSITORY" --name "$name" --dir "$RUNNER_TEMP/artifact-cache-dl" || return 1 + # Each artifact holds a single tarball; match by extension so the inner + # filename is not part of the contract. + tarball="$(find "$RUNNER_TEMP/artifact-cache-dl" -name '*.tar.zst' -print -quit)" + [ -n "$tarball" ] || return 1 + tar -I zstd -x -f "$tarball" -C "$CACHE_PATH" + } + if restore "$PRIMARY" "$PRIMARY_BRANCH"; then + echo "Restored from primary key" + echo "cache-hit=true" >> "$GITHUB_OUTPUT" + elif restore "$FALLBACK" "$FALLBACK_BRANCH"; then + echo "Restored from fallback key" + echo "cache-hit=true" >> "$GITHUB_OUTPUT" + else + echo "No matching artifact found; starting cold" + echo "cache-hit=false" >> "$GITHUB_OUTPUT" + fi + + - name: Pack artifact cache + id: pack + if: ${{ inputs.mode == 'save' }} + shell: bash + env: + CACHE_PATH: ${{ inputs.path }} + run: | + set -euo pipefail + packdir="$(mktemp -d)" # unique dir so concurrent saves cannot collide + tar -I 'zstd -T0' -cf "$packdir/cache.tar.zst" -C "$CACHE_PATH" . + echo "tarball=$packdir/cache.tar.zst" >> "$GITHUB_OUTPUT" + + - name: Upload artifact cache + if: ${{ inputs.mode == 'save' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ${{ steps.name.outputs.primary }} + path: ${{ steps.pack.outputs.tarball }} + retention-days: ${{ inputs.retention-days }} + overwrite: true + compression-level: 0 # tarball is already zstd-compressed diff --git a/.github/actions/setup-venv/action.yml b/.github/actions/setup-venv/action.yml new file mode 100644 index 000000000..3feb9c5be --- /dev/null +++ b/.github/actions/setup-venv/action.yml @@ -0,0 +1,56 @@ +--- +# DESCRIPTION: Github actions composite action +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +name: Set up Python venv +description: >- + Restore, create, and optionally save the make-managed Python venv in a GitHub + Actions cache. The cache key combines the venv path (venvs are not + relocatable), the host OS and version, the Python major.minor version, and + the python-dev-requirements.txt hash, so any of those changing invalidates + it. Requires the already-configured Verilator tree to be present in 'repo'. + +inputs: + repo: + description: "Verilator checkout holding the Makefile and .venv" + required: false + default: repo + save: + description: "Save the cache on a miss (set true only on trusted branches)" + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Compute Python venv cache key + id: cachekey + shell: bash + working-directory: ${{ inputs.repo }} + run: | + source ci/ci-common.bash + host="${DISTRO_ID:-$HOST_OS}${DISTRO_VERSION:+-$DISTRO_VERSION}" + pyver="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" + reqs="$(sha256sum python-dev-requirements.txt | cut -d' ' -f1)" + venv="${PWD#/}/.venv" # absolute venv path (leading '/' trimmed) + echo "key=venv-${host}-py${pyver}-${reqs}-${venv//\//-}" >> "$GITHUB_OUTPUT" + + - name: Restore Python venv + id: venv + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 + with: + path: ${{ inputs.repo }}/.venv + key: ${{ steps.cachekey.outputs.key }} + + - name: Create Python venv + if: ${{ steps.venv.outputs.cache-hit != 'true' }} + shell: bash + working-directory: ${{ inputs.repo }} + run: make venv + + - name: Save Python venv + if: ${{ inputs.save == 'true' && steps.venv.outputs.cache-hit != 'true' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 + with: + path: ${{ inputs.repo }}/.venv + key: ${{ steps.venv.outputs.cache-primary-key }} diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0e6f05cc1..163669117 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -16,6 +16,7 @@ on: permissions: contents: read + actions: read defaults: run: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 00ab5329e..5ff39b99b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,6 +13,7 @@ on: permissions: contents: read + actions: read defaults: run: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f41c513a5..a059439e2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,7 +54,7 @@ jobs: - name: Docker meta id: docker_meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | ${{ vars.DOCKER_HUB_NAMESPACE }}/${{ env.image_name }} @@ -64,21 +64,21 @@ jobs: type=raw,value=latest,enable=${{ inputs.add_latest_tag == true }} - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 with: buildkitd-flags: --debug - name: Login to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Build and Push to Docker - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' with: context: ${{ env.build_context }} diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 693190453..78fd4130a 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,6 +11,10 @@ on: permissions: contents: write +defaults: + run: + working-directory: repo + jobs: format: runs-on: ubuntu-24.04 @@ -19,21 +23,32 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: + path: repo token: ${{ secrets.GITHUB_TOKEN }} + - name: Install packages for build run: | sudo apt install clang-format-18 || \ sudo apt install clang-format-18 git config --global user.email "action@example.com" git config --global user.name "github action" - - name: Format code + + - name: Configure run: | autoconf ./configure - make venv + + - name: Set up Python venv + uses: ./repo/.github/actions/setup-venv + with: + save: ${{ github.ref == 'refs/heads/master' }} + + - name: Format code + run: | source .venv/bin/activate make -j 4 format CLANGFORMAT=clang-format-18 git status + - name: Push run: |- if [ -n "$(git status --porcelain)" ]; then diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 4b26610bc..884d1a1f9 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -45,9 +45,9 @@ on: value: ${{ jobs.build.outputs.archive }} env: - CCACHE_COMPRESS: 1 + CACHE_BASE_KEY: build-${{ inputs.runs-on }}-${{ inputs.cc }}${{ inputs.ccwarn && '-ccwarn' || '' }}${{ inputs.dev-asan && '-asan' || '' }}${{ inputs.dev-gcov && '-gcov' || '' }} + CCACHE_COMPILERCHECK: content CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_LIMIT_MULTIPLE: 0.95 defaults: run: @@ -61,9 +61,6 @@ jobs: runs-on: ${{ inputs.runs-on }} outputs: archive: ${{ steps.create-archive.outputs.archive }} - env: - CACHE_BASE_KEY: build-${{ inputs.runs-on }}-${{ inputs.cc }} - CCACHE_MAXSIZE: 1000M steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -74,7 +71,15 @@ jobs: # behind 'verilator --version' fetch-depth: ${{ (inputs.install || inputs.dev-gcov) && '0' || '1' }} + - name: Configure ccache + run: | + # ccache is unreliable on macOS, and does nothing with 'gcc --coverage' + if [ "${{ startsWith(inputs.runs-on, 'macos') }}" = true ] || [ "${{ inputs.dev-gcov }}" = true ]; then + echo "CCACHE_DISABLE=1" >> "$GITHUB_ENV" + fi + - name: Cache $CCACHE_DIR + if: ${{ env.CCACHE_DISABLE != '1' }} uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 env: CACHE_KEY: ${{ env.CACHE_BASE_KEY }}-ccache @@ -82,7 +87,7 @@ jobs: path: ${{ env.CCACHE_DIR }} key: ${{ env.CACHE_KEY }}-${{ inputs.sha }} restore-keys: | - ${{ env.CACHE_KEY }}- + ${{ env.CACHE_KEY }} - name: Install packages for build run: ./ci/ci-install.bash build diff --git a/.github/workflows/reusable-lint-py.yml b/.github/workflows/reusable-lint-py.yml index ae7ff30b6..e3bea6fe7 100644 --- a/.github/workflows/reusable-lint-py.yml +++ b/.github/workflows/reusable-lint-py.yml @@ -7,11 +7,6 @@ name: reusable-lint-py on: workflow_call: -env: - CCACHE_COMPRESS: 1 - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_LIMIT_MULTIPLE: 0.95 - defaults: run: shell: bash @@ -28,19 +23,18 @@ jobs: with: path: repo - - name: Install packages for build - run: ./ci/ci-install.bash build + - name: Install dependencies + run: ./ci/ci-install.bash lint-py - name: Configure run: | autoconf ./configure --enable-longtests --enable-ccwarn - - name: Install python dependencies - run: | - sudo apt install python3-clang || \ - sudo apt install python3-clang - make venv + - name: Set up Python venv + uses: ./repo/.github/actions/setup-venv + with: + save: ${{ github.ref == 'refs/heads/master' }} - name: Lint run: |- diff --git a/.github/workflows/reusable-rtlmeter-run.yml b/.github/workflows/reusable-rtlmeter-run.yml index 55cc4bc55..4d8fbd9b1 100644 --- a/.github/workflows/reusable-rtlmeter-run.yml +++ b/.github/workflows/reusable-rtlmeter-run.yml @@ -53,8 +53,8 @@ defaults: shell: bash env: - CCACHE_DIR: ${{ github.workspace }}/ccache - CCACHE_MAXSIZE: 512M + # RTLMeter measures compile/execute times, so caching must stay off to keep + # timings representative; ccache is still on PATH but acts as a pass-through CCACHE_DISABLE: 1 jobs: @@ -82,14 +82,6 @@ jobs: working-directory: rtlmeter run: make venv - - name: Use saved ccache - if: ${{ env.CCACHE_DISABLE == 0 }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 - with: - path: ${{ env.CCACHE_DIR }} - key: rtlmeter-run-ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.cases }}-${{ inputs.compileArgs }}-${{ github.run_id }}-${{ github.run_attempt }} - restore-keys: rtlmeter-run-ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.cases }}-${{ inputs.compileArgs }} - ######################################################################## # Run with new Verilator ######################################################################## diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 8782f17e4..bd5a76c7c 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -34,10 +34,14 @@ on: required: true type: string +permissions: + contents: read + actions: read + env: - CCACHE_COMPRESS: 1 + CCACHE_COMPILERCHECK: content CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_LIMIT_MULTIPLE: 0.95 + CXX: ${{ inputs.cc == 'clang' && 'clang++' || 'g++' }} defaults: run: @@ -49,10 +53,6 @@ jobs: test: runs-on: ${{ inputs.runs-on }} name: Test - env: - CXX: ${{ inputs.cc == 'clang' && 'clang++' || 'g++' }} - CACHE_BASE_KEY: test-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.reloc }}-${{ inputs.suite }} - CCACHE_MAXSIZE: 100M # Per build per suite (* 5 * 5 = 2500M in total) steps: - name: Download repository archive @@ -67,20 +67,31 @@ jobs: tar --zstd -x -f ${{ inputs.archive }} ls -lsha - - name: Cache $CCACHE_DIR - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 - env: - CACHE_KEY: ${{ env.CACHE_BASE_KEY }}-ccache2 + - name: Configure ccache + run: | + # ccache is unreliable on macOS, and does nothing with 'gcc --coverage' + if [ "${{ startsWith(inputs.runs-on, 'macos') }}" = true ] || [ "${{ inputs.dev-gcov }}" = true ]; then + echo "CCACHE_DISABLE=1" >> "$GITHUB_ENV" + fi + + # Test-job ccache is stored as an artifact, not actions/cache + - name: Restore ccache + id: ccache + if: ${{ env.CCACHE_DISABLE != '1' }} + uses: ./repo/.github/actions/artifact-cache with: + mode: restore path: ${{ env.CCACHE_DIR }} - key: ${{ env.CACHE_KEY }}-${{ github.sha }} - restore-keys: | - ${{ env.CACHE_KEY }}- + key: ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.suite }} - name: Install test dependencies run: | ./ci/ci-install.bash test - make venv + + - name: Set up Python venv + uses: ./repo/.github/actions/setup-venv + with: + save: ${{ github.ref == 'refs/heads/master' }} - name: Test id: run-test @@ -105,6 +116,16 @@ jobs: path: ${{ github.workspace }}/repo/obj_coverage/verilator-${{ inputs.suite }}.info name: code-coverage-${{ inputs.suite }} + - name: Save ccache + if: ${{ env.CCACHE_DISABLE != '1' && !cancelled() }} + uses: ./repo/.github/actions/artifact-cache + with: + mode: save + path: ${{ env.CCACHE_DIR }} + key: ${{ steps.ccache.outputs.key }} + # Keep master's cache around longer; PR/branch caches churn faster + retention-days: ${{ github.ref == 'refs/heads/master' && 7 || 3 }} + - name: Fail job if a test failed if: ${{ steps.run-test.outcome == 'failure' && !cancelled() }} run: |- diff --git a/ci/ci-build.bash b/ci/ci-build.bash index 722ed6563..64f037b33 100755 --- a/ci/ci-build.bash +++ b/ci/ci-build.bash @@ -60,7 +60,7 @@ esac ################################################################################ # Configure -CONFIGURE_ARGS="--prefix=$OPT_PREFIX --enable-longtests" +CONFIGURE_ARGS="--prefix=$OPT_PREFIX --enable-longtests --enable-light-debug" if [ "$OPT_CCWARN" = 1 ]; then CONFIGURE_ARGS="$CONFIGURE_ARGS --enable-ccwarn" fi @@ -78,5 +78,8 @@ autoconf # Build ccache -z +BUILD_START=$SECONDS "$MAKE" -j "$NPROC" -k ccache -svv +ccache --evict-older-than "$((SECONDS - BUILD_START + 60))s" +ccache -svv diff --git a/ci/ci-common.bash b/ci/ci-common.bash index ef2fe23c8..1bc8826a3 100644 --- a/ci/ci-common.bash +++ b/ci/ci-common.bash @@ -26,8 +26,6 @@ case "$(uname -s)" in HOST_OS=macOS MAKE=make NPROC=$(sysctl -n hw.logicalcpu) - # Disable ccache, doesn't always work in GitHub Actions - export OBJCACHE= ;; *) fatal "Unknown host OS: '$(uname -s)'" diff --git a/ci/ci-install.bash b/ci/ci-install.bash index 1f58184ea..fa14be495 100755 --- a/ci/ci-install.bash +++ b/ci/ci-install.bash @@ -41,6 +41,11 @@ if [ "$HOST_OS" = "linux" ]; then echo "path-exclude /usr/share/doc/*" | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc echo "path-exclude /usr/share/man/*" | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc echo "path-exclude /usr/share/info/*" | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc +elif [ "$HOST_OS" = "macOS" ]; then + # The macos runner image ships an untrusted third-party tap we don't use; + # untap it so brew stops emitting a tap-trust warning. Force + '|| true' since + # untap fails if a formula was installed from it, which is harmless here. + brew untap --force aws/tap || true fi install-wavediff() { @@ -146,10 +151,21 @@ elif [ "$STAGE" = "test" ]; then install-wavediff # Workaround -fsanitize=address crash sudo sysctl -w vm.mmap_rnd_bits=28 +elif [ "$STAGE" = "lint-py" ]; then + # nodist/clang_check_attributes. + if [ "$HOST_OS" = "linux" ] && [ "$DISTRO_ID" = "ubuntu" ]; then + PACKAGES=( + python3-clang # Not run, but importers are linted + ) + sudo apt-get update || + sudo apt-get update + sudo apt-get install --yes "${PACKAGES[@]}" || + sudo apt-get install --yes "${PACKAGES[@]}" + fi else ############################################################################## # Unknown build stage - fatal "Unknown stage '$STAGE' (expected 'build' or 'test')" + fatal "Unknown stage '$STAGE' (expected 'build', 'test' or 'lint-py')" fi # Report where the tools we may have installed live (ok if some are missing) diff --git a/ci/ci-test.bash b/ci/ci-test.bash index f35257b94..9d0b69517 100755 --- a/ci/ci-test.bash +++ b/ci/ci-test.bash @@ -8,7 +8,7 @@ # Executed in the 'test' stage. ################################################################################ -# Destructive to the checkout (runs 'find . -delete'); never run locally +# Destructive to the checkout (wipes most of it); never run locally if [ "$GITHUB_ACTIONS" != "true" ]; then echo "ERROR: $(basename "$0") must only be run in GitHub Actions CI" >&2 exit 1 @@ -66,13 +66,14 @@ if [ -n "$OPT_RELOC" ]; then mv test_regress "$TEST_REGRESS" NODIST="$OPT_RELOC/nodist" mv nodist "$NODIST" - # Delete everything else - find . -delete + # Delete everything else, but keep the CI infrastructure + find . -mindepth 1 -maxdepth 1 ! -name .github ! -name ci -exec rm -rf {} + ls -la . fi # Run the specified suite ccache -z +TEST_START=$SECONDS case $OPT_SUITE in dist-vlt-0) "$MAKE" -C "$TEST_REGRESS" SCENARIOS="--dist --vlt --driver-clean" DRIVER_HASHSET=--hashset=0/4 @@ -163,4 +164,6 @@ case $OPT_SUITE in ;; esac ccache -svv +ccache --evict-older-than "$((SECONDS - TEST_START + 60))s" +ccache -svv uptime # To see load average diff --git a/configure.ac b/configure.ac index 6376593ad..893bf91d0 100644 --- a/configure.ac +++ b/configure.ac @@ -129,6 +129,21 @@ AC_ARG_ENABLE([dev-gcov], AC_SUBST(CFG_WITH_DEV_GCOV) AC_MSG_RESULT($CFG_WITH_DEV_GCOV) +# Flag to reduce the debug info in the debug executable to minimize its size +AC_MSG_CHECKING(whether to reduce debug info in the debug executable) +AC_ARG_ENABLE([light-debug], + [AS_HELP_STRING([--enable-light-debug], + [Reduce the amount of debug information in the debug + Verilator executable to minimize its size. Enable + slight optimization. Enough for backtraces only.])], + [case "${enableval}" in + yes) CFG_WITH_LIGHT_DEBUG=yes ;; + no) CFG_WITH_LIGHT_DEBUG=no ;; + *) AC_MSG_ERROR([bad value '${enableval}' for --enable-light-debug]) ;; + esac], + CFG_WITH_LIGHT_DEBUG=no) +AC_MSG_RESULT($CFG_WITH_LIGHT_DEBUG) + # Special Substitutions - CFG_WITH_DEFENV AC_MSG_CHECKING(whether to use hardcoded paths) AC_ARG_ENABLE([defenv], @@ -518,15 +533,27 @@ _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_PARSER,-Wno-unused) AC_SUBST(CFG_CXXFLAGS_PARSER) # Flags for compiling the debug version of Verilator (in addition to above CFG_CXXFLAGS_SRC) -if test "$CFG_WITH_DEV_GCOV" = "no"; then # Do not optimize for the coverage build -_MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-Og) +if test "$CFG_WITH_LIGHT_DEBUG" = "yes"; then + # Slight optimization and minimal compressed debug info. This is enough for + # --gdb/--gdbbt backtraces, but omits the bulk of the debug info, which + # significantly reduces object file sizes. For CI or release builds. + if test "$CFG_WITH_DEV_GCOV" = "no"; then # Do not optimize for the coverage build + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-Og) + fi + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-ggdb1) + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-fdebug-info-for-profiling) # For fully-qualified names with clang + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-gz) +else + # Full debug: no optimization, full debug info, for development. + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-O0) + _MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-ggdb) fi -_MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-ggdb) -_MY_CXX_CHECK_OPT(CFG_CXXFLAGS_DBG,-gz) AC_SUBST(CFG_CXXFLAGS_DBG) # Flags for linking the debug version of Verilator (in addition to above CFG_LDFLAGS_SRC) -_MY_LDLIBS_CHECK_OPT(CFG_LDFLAGS_DBG,-gz) +if test "$CFG_WITH_LIGHT_DEBUG" = "yes"; then + _MY_LDLIBS_CHECK_OPT(CFG_LDFLAGS_DBG,-gz) +fi AC_SUBST(CFG_LDFLAGS_DBG) # Flags for compiling the optimized version of Verilator (in addition to above CFG_CXXFLAGS_SRC) diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index 24e3ced87..62f8d0bc3 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -220,6 +220,7 @@ Nikolai Kumar Nikolay Puzanov Nolan Poe Oleh Maksymenko +Patrick Creighton Patrick Stewart Paul Bowen-Huggett Paul Swirhun diff --git a/docs/guide/extensions.rst b/docs/guide/extensions.rst index 8e8d5d183..3cc41ea46 100644 --- a/docs/guide/extensions.rst +++ b/docs/guide/extensions.rst @@ -294,6 +294,25 @@ or "`ifdef`"'s may break other tools. (if appropriate :vlopt:`--coverage` flags are passed) after being disabled earlier with :option:`/*verilator&32;coverage_off*/`. +.. option:: /*verilator&32;dpi_c_decl ""*/ + + Specifies C function declaration that will be emitted for given DPI-C + function into the Verilator-generated __Dpi.h header, replacing the declaration + that Verilator would build by default using the Verilog function signature. + + This enables use of C functions with types not specified in the + standard. For example, it enables use of functions that return ``char*``: + + .. code-block:: sv + + module t; + import "DPI-C" function string getenv(input string arg) /*verilator dpi_c_decl "char* getenv(const char*)"*/; + + initial begin + $display("%s", getenv("HOME")); + end + endmodule + .. option:: /*verilator&32;fargs */ For Verilator developers only. When a source file containing these `fargs` diff --git a/include/verilated_fst_c.cpp b/include/verilated_fst_c.cpp index 0e3706725..de54d9e8e 100644 --- a/include/verilated_fst_c.cpp +++ b/include/verilated_fst_c.cpp @@ -98,8 +98,10 @@ void VerilatedFst::close() VL_MT_SAFE_EXCLUDES(m_mutex) { const VerilatedLockGuard lock{m_mutex}; Super::closeBase(); emitTimeChangeMaybe(); - if (m_fst) m_fst->close(); // LCOV_EXCL_BR_LINE - m_fst = nullptr; + if (m_fst) { + m_fst->close(); + VL_DO_CLEAR(delete m_fst, m_fst = nullptr); + } } void VerilatedFst::flush() VL_MT_SAFE_EXCLUDES(m_mutex) { diff --git a/python-dev-requirements.txt b/python-dev-requirements.txt index 3e33332cc..e90349d2c 100644 --- a/python-dev-requirements.txt +++ b/python-dev-requirements.txt @@ -33,4 +33,4 @@ sphinxcontrib-spelling==8.0.2 yamlfix==1.19.1 yapf==0.43.0 -git+https://github.com/antmicro/astsee.git +git+https://github.com/antmicro/astsee.git@222480a8ec13b312809ea4acc08c81b4c0f4da2f diff --git a/src/V3Assert.cpp b/src/V3Assert.cpp index e7724cfda..cbf462260 100644 --- a/src/V3Assert.cpp +++ b/src/V3Assert.cpp @@ -90,6 +90,58 @@ public: explicit DefaultDisablePropagateVisitor(AstNetlist* nodep) { iterate(nodep); } }; +// Lower a sequence used as an event control ('@seq', IEEE 1800-2023 9.4.2.4) into a +// synthesized event fired by an internal 'cover sequence' on each end-of-match +class SeqEventLowerVisitor final : public VNVisitor { + // STATE + AstNodeModule* m_modp = nullptr; // Current module + V3UniqueNames m_names{"__VseqEvent"}; // Synthesized event names + + // VISITORS + void visit(AstNodeModule* nodep) override { + VL_RESTORER(m_modp); + m_modp = nodep; + iterateChildren(nodep); + } + void visit(AstSenItem* nodep) override { + AstFuncRef* const funcrefp = VN_CAST(nodep->sensp(), FuncRef); + if (funcrefp && VN_IS(funcrefp->taskp(), Sequence)) { + FileLine* const flp = nodep->fileline(); + AstVar* const eventp = new AstVar{flp, VVarType::MODULETEMP, m_names.get(nodep), + m_modp->findBasicDType(VBasicDTypeKwd::EVENT)}; + eventp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(eventp); + v3Global.setHasEvents(); + funcrefp->unlinkFrBack(); + nodep->sensp(new AstVarRef{flp, eventp, VAccess::READ}); + const bool automaticActual = funcrefp->exists([](const AstNodeVarRef* refp) { + return refp->varp() && refp->varp()->lifetime().isAutomatic(); + }); + if (automaticActual) { + nodep->v3error("Arguments to a sequence used as an event control must be" + " static (IEEE 1800-2023 9.4.2.4)"); + VN_AS(funcrefp->taskp(), Sequence)->isReferenced(false); + VL_DO_DANGLING(pushDeletep(funcrefp), funcrefp); + return; + } + AstFireEvent* const firep + = new AstFireEvent{flp, new AstVarRef{flp, eventp, VAccess::WRITE}, false}; + AstCover* const coverp + = new AstCover{flp, new AstPropSpec{flp, nullptr, nullptr, funcrefp}, firep, + VAssertType::CONCURRENT}; + coverp->isCoverSeq(true); + coverp->isSeqEvent(true); + m_modp->addStmtsp(coverp); + return; + } + iterateChildren(nodep); + } + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + explicit SeqEventLowerVisitor(AstNetlist* nodep) { iterate(nodep); } +}; + } // namespace void V3AssertCommon::collectDefaultDisable(AstNetlist* nodep) { @@ -97,6 +149,11 @@ void V3AssertCommon::collectDefaultDisable(AstNetlist* nodep) { { DefaultDisablePropagateVisitor{nodep}; } } +void V3AssertCommon::lowerSequenceEvents(AstNetlist* nodep) { + { SeqEventLowerVisitor{nodep}; } + V3Global::dumpCheckGlobalTree("assertseqevent", 0, dumpTreeEitherLevel() >= 3); +} + //###################################################################### // AssertDeFutureVisitor // If any AstFuture, then move all non-future varrefs to be one cycle behind, @@ -546,14 +603,19 @@ class AssertVisitor final : public VNVisitor { bool selfDestruct = false; bool passspGated = false; - if (const AstCover* const snodep = VN_CAST(nodep, Cover)) { + const AstCover* const coverp = VN_CAST(nodep, Cover); + // A sequence event control is not an assertion directive; no assertion control + const bool seqEvent = coverp && coverp->isSeqEvent(); + if (coverp) { ++m_statCover; - if (!v3Global.opt.coverageUser()) { + if (seqEvent) { + // Keep the event-fire action, with no coverage bucket + } else if (!v3Global.opt.coverageUser()) { selfDestruct = true; } else { // V3Coverage assigned us a bucket to increment. - AstCoverInc* const covincp = VN_AS(snodep->coverincsp(), CoverInc); - UASSERT_OBJ(covincp, snodep, "Missing AstCoverInc under assertion"); + AstCoverInc* const covincp = VN_AS(coverp->coverincsp(), CoverInc); + UASSERT_OBJ(covincp, coverp, "Missing AstCoverInc under assertion"); covincp->unlinkFrBackWithNext(); // next() might have AstAssign for trace if (message != "") covincp->declp()->comment(message); if (passsp) { @@ -597,7 +659,8 @@ class AssertVisitor final : public VNVisitor { FileLine* const flp = nodep->fileline(); bool passspAlreadyGated = false; if (passsp && VN_IS(passsp, If)) passspAlreadyGated = VN_AS(passsp, If)->user1(); - if (passsp && !passspGated && !passspAlreadyGated && !VN_IS(propExprp, PExpr)) { + if (passsp && !passspGated && !passspAlreadyGated && !VN_IS(propExprp, PExpr) + && !seqEvent) { passsp = newIfAssertPassOn(passsp, nodep->directive(), nodep->userType(), /*vacuous=*/false); } @@ -607,7 +670,7 @@ class AssertVisitor final : public VNVisitor { AstNode* bodysp = assertBody(nodep, propExprp, passsp, failsp); if (disablep) bodysp = new AstIf{flp, new AstLogNot{flp, disablep}, bodysp}; // Add assertOn check last, for better combining - bodysp = newIfAssertOn(bodysp, nodep->directive(), nodep->userType()); + if (!seqEvent) bodysp = newIfAssertOn(bodysp, nodep->directive(), nodep->userType()); if (sentreep) bodysp = new AstAlways{flp, VAlwaysKwd::ALWAYS, sentreep, bodysp}; if (passsp && !passsp->backp()) VL_DO_DANGLING(pushDeletep(passsp), passsp); diff --git a/src/V3Assert.h b/src/V3Assert.h index f144688b6..3bda2528e 100644 --- a/src/V3Assert.h +++ b/src/V3Assert.h @@ -27,6 +27,7 @@ class V3AssertCommon final { public: static void collectDefaultDisable(AstNetlist* nodep) VL_MT_DISABLED; + static void lowerSequenceEvents(AstNetlist* nodep) VL_MT_DISABLED; }; class V3Assert final { diff --git a/src/V3AssertNfa.cpp b/src/V3AssertNfa.cpp index 6a35273a5..552eabd48 100644 --- a/src/V3AssertNfa.cpp +++ b/src/V3AssertNfa.cpp @@ -48,8 +48,8 @@ class SvaStateVertex; // Per-vertex algorithm data, stored via V3GraphVertex::userp() during lowering struct SvaVertexData final { AstVar* stateVarp = nullptr; // NBA state register for this vertex - AstVar* counterActiveVarp = nullptr; // Counter FSM active flag - AstVar* counterCountVarp = nullptr; // Counter FSM count register + AstVar* delayRingVarp = nullptr; // Bitset ring buffer + AstVar* delayRingIdxVarp = nullptr; // Next slot written in delayRingVarp AstVar* doneLVarp = nullptr; // SAnd LHS done-latch AstVar* doneRVarp = nullptr; // SAnd RHS done-latch AstNodeExpr* stateSigp = nullptr; // Combinational state signal (owned during lowering) @@ -64,10 +64,10 @@ public: bool m_isMatch = false; // Owned throughout-guard condition clones; IEEE 1800-2023 16.9.9 std::vector m_throughoutConds; - // Counter FSM vertex for ##[M:N] when N-M > kChainLimit - bool m_isCounter = false; - int m_counterMax - = 0; // Counter window maximum (min is always 0; pre-chain handles the M offset) + // Nonzero for a bitset ring-buffer vertex for ## delays. + bool m_isFixedDelayRing = false; + int m_delayRingSize = 0; // Fixed delay cycles. Range: max-min+1. + AstNodeExpr* m_delayRingClearCondp = nullptr; // local RHS for pure-boolean range // Liveness terminal (IEEE weak semantics): reject must not fire from this source bool m_isUnbounded = false; // Temporal sequence AND combiner; IEEE 1800-2023 16.9.5 @@ -88,15 +88,25 @@ public: : V3GraphVertex{graphp} {} ~SvaStateVertex() override { for (AstNodeExpr* cp : m_throughoutConds) VL_DO_DANGLING(cp->deleteTree(), cp); + if (m_delayRingClearCondp) + VL_DO_DANGLING(m_delayRingClearCondp->deleteTree(), m_delayRingClearCondp); if (m_andLhsCondp) VL_DO_DANGLING(m_andLhsCondp->deleteTree(), m_andLhsCondp); if (m_andRhsCondp) VL_DO_DANGLING(m_andRhsCondp->deleteTree(), m_andRhsCondp); } // METHODS - string name() const override { return "s" + cvtToStr(color()); } // LCOV_EXCL_LINE // LCOV_EXCL_START -- Graphviz dump only + string name() const override { + string name = "s" + cvtToStr(color()); + if (m_delayRingSize) { + name += "\\n"; + name += m_isFixedDelayRing ? "fixed chain " : "range chain "; + name += cvtToStr(m_delayRingSize) + " bits"; + } + return name; + } string dotColor() const override { if (m_isMatch) return "red"; - if (m_isCounter) return "blue"; + if (m_delayRingSize) return "blue"; if (m_isAndCombiner) return "purple"; return "black"; } @@ -287,6 +297,8 @@ class SvaNfaBuilder final { // not just the first. Builder builds parallel-branch (no first-match-wins) // topology when true. Default false preserves cover_property semantics. bool m_isCoverSeq = false; + // Unsupported endpoint topology must reject, not ignore, or the wait hangs + bool m_isSeqEvent = false; struct RangeDelayRejectInfo final { SvaStateVertex* startp = nullptr; @@ -294,6 +306,15 @@ class SvaNfaBuilder final { int rhsLen = 0; }; + void warnEndpointUnsupported(FileLine* flp, const string& what) const { + if (m_isSeqEvent) { + flp->v3warn(E_UNSUPPORTED, + "Unsupported: sequence used as an event control with " << what); + } else { + flp->v3warn(COVERIGN, "Ignoring unsupported: cover sequence with " << what); + } + } + AstNodeExpr* throughoutCond(AstNodeExpr* baseCondp, FileLine* flp) { if (m_throughoutStack.empty()) return baseCondp; // AND all throughout conditions (supports nesting) @@ -461,8 +482,8 @@ class SvaNfaBuilder final { AstVar* tryHoistSampled(AstNodeExpr* exprp, FileLine* flp, int cloneCount) { constexpr int kHoistThreshold = 2; if (cloneCount < kHoistThreshold) return nullptr; - AstVar* const tempVarp = new AstVar{flp, VVarType::MODULETEMP, m_propTempNames.get(exprp), - m_modp->findBitDType()}; + AstVar* const tempVarp + = new AstVar{flp, VVarType::MODULETEMP, m_propTempNames.get(exprp), exprp->dtypep()}; m_modp->addStmtsp(tempVarp); AstAssign* const assignp = new AstAssign{flp, new AstVarRef{flp, tempVarp, VAccess::WRITE}, sampled(exprp->cloneTreePure(false))}; @@ -513,14 +534,28 @@ class SvaNfaBuilder final { return m_graph.addClockedEdge(fromp, top, throughoutCond(nullptr, flp)); } - SvaStateVertex* addDelayChain(SvaStateVertex* startp, int n, FileLine* flp) { - SvaStateVertex* currentp = startp; - for (int i = 0; i < n; ++i) { + SvaStateVertex* addDelayChain(SvaStateVertex* startp, int size, FileLine* flp, + bool isFixed = true, AstNodeExpr* clearCondp = nullptr) { + if (isFixed && size == 0) return startp; + UASSERT_OBJ(size > 0, startp, "Delay chain needs at least one slot"); + if (isFixed && size == 1) { SvaStateVertex* const nextp = scopedCreateVertex(); - guardedEdge(currentp, nextp, flp); - currentp = nextp; + guardedEdge(startp, nextp, flp); + return nextp; } - return currentp; + SvaStateVertex* const ringVtxp = scopedCreateVertex(); + ringVtxp->m_isFixedDelayRing = isFixed; + ringVtxp->m_delayRingSize = size; + if (clearCondp) { + UASSERT_OBJ(!isFixed, startp, "Fixed delay cannot have a clear condition"); + ringVtxp->m_delayRingClearCondp = clearCondp->cloneTreePure(false); + } + if (isFixed) { + guardedEdge(startp, ringVtxp, flp); + } else { + guardedLink(startp, ringVtxp, flp); + } + return ringVtxp; } // Build NFA for an SExpr. finalCond = RHS (not yet added as a vertex). @@ -565,7 +600,7 @@ class SvaNfaBuilder final { const int range = maxDelay - minDelay; currentp = addDelayChain(currentp, minDelay, flp); // kChainLimit bounds per-attempt unrolled vertices. Above this, a - // counter FSM (constant-size state) is used instead, so the vertex + // ring buffer (constant-size state) is used instead, so the vertex // count is O(1) in range regardless of user input; no adversarial N // blowup is possible. constexpr int kChainLimit = 256; @@ -574,19 +609,13 @@ class SvaNfaBuilder final { // overlapping ends and the nested-sequence merge collapses them, so // reject those for a cover sequence rather than under-count. if (m_isCoverSeq && (range > kChainLimit || VN_IS(rhsExprp, SExpr))) { - flp->v3warn(COVERIGN, - "Ignoring unsupported: cover sequence with this ranged cycle delay"); + warnEndpointUnsupported(flp, "this ranged cycle delay"); outErrorEmitted = true; return false; } if (range > kChainLimit) { - // Large range: counter FSM. Overlapping triggers during an active - // count are dropped (non-overlapping semantics only). - SvaStateVertex* const counterVtxp = scopedCreateVertex(); - counterVtxp->m_isCounter = true; - counterVtxp->m_counterMax = range; - guardedEdge(currentp, counterVtxp, flp); - currentp = counterVtxp; + currentp = addDelayChain(currentp, range + 1, flp, false, + rhsExprp->isMultiCycleSva() ? nullptr : rhsExprp); } else if (VN_IS(rhsExprp, SExpr)) { // Nested-SExpr RHS: merge all [M,N] positions. Candidate-local misses // are not assertion rejects while a later position can still match. @@ -881,8 +910,7 @@ class SvaNfaBuilder final { // terminal, so a cover sequence would under-count. Reject the ranged form // (the single-count b[->N] has one end and is enumerated correctly). if (m_isCoverSeq && hasMax && maxN > minN) { - flp->v3warn(COVERIGN, - "Ignoring unsupported: cover sequence with a ranged goto repetition"); + warnEndpointUnsupported(flp, "a ranged goto repetition"); return BuildResult::failWithError(); } @@ -946,8 +974,7 @@ class SvaNfaBuilder final { // than under-count. Plain boolean disjunction has one end per cycle and // is handled by the OR-fold. if (m_isCoverSeq && (lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp)) { - flp->v3warn(COVERIGN, - "Ignoring unsupported: cover sequence with a sequence operand of 'or'"); + warnEndpointUnsupported(flp, "a sequence operand of 'or'"); return BuildResult::failWithError(); } SvaStateVertex* const mergeVtxp = scopedCreateVertex(); @@ -1442,11 +1469,12 @@ class SvaNfaBuilder final { public: SvaNfaBuilder(SvaGraph& graph, AstNodeModule* modp, V3UniqueNames& propTempNames, - bool isCoverSeq = false) + bool isCoverSeq = false, bool isSeqEvent = false) : m_graph{graph} , m_modp{modp} , m_propTempNames{propTempNames} - , m_isCoverSeq{isCoverSeq} {} + , m_isCoverSeq{isCoverSeq} + , m_isSeqEvent{isSeqEvent} {} // Reset scope between antecedent and consequent: liveness must not leak. void resetScope() { @@ -1640,6 +1668,25 @@ class SvaNfaLowering final { if (!exprp) return nullptr; return new AstLogAnd{c.flp, exprp, notKillActive(c)}; } + static AstNodeExpr* nextRingIndex(FileLine* flp, AstVar* idxp, uint32_t size) { + const auto u32Const = [flp](uint32_t value) { + return new AstConst{flp, AstConst::WidthedValue{}, 32, value}; + }; + UASSERT(size > 1, "Delay ring index needs at least two slots"); + // idx == size - 1 ? 0 : idx + 1 + AstAdd* const addp = new AstAdd{flp, new AstVarRef{flp, idxp, VAccess::READ}, u32Const(1)}; + addp->dtypeFrom(idxp); + AstCond* const condp = new AstCond{ + flp, new AstEq{flp, new AstVarRef{flp, idxp, VAccess::READ}, u32Const(size - 1)}, + u32Const(0), addp}; + condp->dtypeFrom(idxp); + return condp; + } + static AstNodeExpr* delayRingBit(FileLine* flp, AstVar* ringp, AstNodeExpr* idxExprp, + VAccess access = VAccess::READ) { + // ring[idx] + return new AstSel{flp, new AstVarRef{flp, ringp, access}, idxExprp, 1}; + } // Phase 3 output signals struct SignalSet final { @@ -1650,12 +1697,14 @@ class SvaNfaLowering final { }; // Phase 2/2b/2c: Emit NBA state-update always blocks for registered vertices, - // counter FSMs, and SAnd combiner done-latches. + // delay rings, and SAnd combiner done-latches. // Phase 2: State register NBA always block. Each clocked-edge target // latches the OR of its incoming contributions. void emitStateRegisterNba(LowerCtx& c) { AstNode* bodyp = nullptr; + bool hasDelayRing = false; for (int i = 0; i < c.N; ++i) { + if (c.vtx[i]->datap()->delayRingVarp) hasDelayRing = true; if (!c.vtx[i]->datap()->stateVarp) continue; AstNodeExpr* nextStatep = nullptr; @@ -1684,100 +1733,104 @@ class SvaNfaLowering final { AstAssignDly* const assignp = new AstAssignDly{ c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::WRITE}, nextStatep}; - if (!bodyp) { - bodyp = assignp; - } else { - bodyp->addNext(assignp); - } + bodyp = AstNode::addNextNull(bodyp, assignp); } - if (!bodyp) return; // Capture disableCnt in Phase-2 NBA before any reactive re-evaluation. // snapshotVarp and disableCntVarp are allocated together. - if (c.snapshotVarp) { + if (c.snapshotVarp && (bodyp || hasDelayRing)) { UASSERT_OBJ(c.disableCntVarp, c.senTreep, "snapshotVarp set without disableCntVarp"); - bodyp->addNext( - new AstAssignDly{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::WRITE}, - new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}); + // disable_snapshot <= disable_count; + AstAssignDly* const snapshotp + = new AstAssignDly{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::WRITE}, + new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}; + bodyp = AstNode::addNextNull(bodyp, snapshotp); } + if (!bodyp) return; m_modp->addStmtsp( new AstAlways{c.flp, VAlwaysKwd::ALWAYS, c.senTreep->cloneTree(false), bodyp}); } - // Phase 2b: Counter FSM always block. - // if (active) { if (done) active<=0; else counter<=counter+1; } - // else if (incoming) { active<=1; counter<=0; } - void emitCounterFsmNba(LowerCtx& c) { - for (int ci = 0; ci < c.N; ++ci) { - if (!c.vtx[ci]->datap()->counterActiveVarp) continue; - AstVar* const activep = c.vtx[ci]->datap()->counterActiveVarp; - AstVar* const cntp = c.vtx[ci]->datap()->counterCountVarp; - const uint32_t counterMax = static_cast(c.vtx[ci]->m_counterMax); + // Phase 2b: Bitset ring-buffer delay always block. + void emitDelayRingNba(LowerCtx& c) { + for (int ri = 0; ri < c.N; ++ri) { + SvaStateVertex* const vtxp = c.vtx[ri]; + if (!vtxp->datap()->delayRingVarp) continue; + AstVar* const ringp = vtxp->datap()->delayRingVarp; + AstVar* const idxp = vtxp->datap()->delayRingIdxVarp; + const uint32_t size = static_cast(vtxp->m_delayRingSize); - // Builder only adds clocked edges to counter vertices (guardedEdge - // in buildSExpr), so m_consumesCycle is always true here. AstNodeExpr* incomingp = nullptr; for (const SvaTransEdge* const tep : c.edges) { - const int toIdx = tep->toVtxp()->color(); - if (toIdx != ci) continue; + if (static_cast(tep->toVtxp()->color()) != ri) continue; + UASSERT_OBJ(tep->m_consumesCycle == vtxp->m_isFixedDelayRing, vtxp, + "Delay-ring incoming edge kind mismatch"); const int fi = tep->fromVtxp()->color(); UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp, c.vtx[fi], - "Clocked edge source missing stateSig"); + "Delay-ring incoming source missing stateSig"); AstNodeExpr* contribp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false); contribp = andCond(c.flp, contribp, tep->m_condp); - if (c.disableExprp) { + if (c.disableExprp && !c.snapshotVarp) { AstNodeExpr* const notDisp = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; contribp = new AstLogAnd{c.flp, contribp, notDisp}; } incomingp = orExprs(c.flp, incomingp, contribp); } - UASSERT_OBJ(incomingp, c.vtx[ci], "Counter vertex has no incoming contribution"); - - // Counter window is always [0, m_counterMax]; M offset is handled by - // the pre-chain in buildSExpr, so every tick inside an active count - // is in-window. - AstNodeExpr* inWindowp = new AstConst{c.flp, AstConst::BitTrue{}}; - AstNodeExpr* matchedNowp = nullptr; - if (c.matchCondp) { - matchedNowp - = new AstLogAnd{c.flp, inWindowp, sampled(c.matchCondp->cloneTreePure(false))}; - } else { // LCOV_EXCL_LINE -- no counter-FSM caller leaves matchCondp null - matchedNowp = inWindowp; // LCOV_EXCL_LINE + UASSERT_OBJ(incomingp, vtxp, "Delay ring has no incoming edge"); + AstNode* updateBodyp = nullptr; + if (vtxp->m_isFixedDelayRing) { + // ring[idx] <= incoming; + updateBodyp = new AstAssignDly{ + c.flp, + delayRingBit(c.flp, ringp, new AstVarRef{c.flp, idxp, VAccess::READ}, + VAccess::WRITE), + incomingp}; + } else { + // ring[next_idx] <= 1'b0; ring[idx] <= incoming; + AstAssignDly* const clearExpirep = new AstAssignDly{ + c.flp, + delayRingBit(c.flp, ringp, nextRingIndex(c.flp, idxp, size), VAccess::WRITE), + new AstConst{c.flp, AstConst::BitFalse{}}}; + AstAssignDly* const writeIncomingp = new AstAssignDly{ + c.flp, + delayRingBit(c.flp, ringp, new AstVarRef{c.flp, idxp, VAccess::READ}, + VAccess::WRITE), + incomingp}; + clearExpirep->addNext(writeIncomingp); + updateBodyp = clearExpirep; } - AstNodeExpr* const counterAtEndp - = new AstEq{c.flp, new AstVarRef{c.flp, cntp, VAccess::READ}, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, counterMax}}; + AstNodeExpr* clearCondp = nullptr; + if (vtxp->m_delayRingClearCondp) { + clearCondp = sampled(vtxp->m_delayRingClearCondp->cloneTreePure(false)); + } + if (c.disableExprp && !c.snapshotVarp) { + clearCondp = orExprs(c.flp, clearCondp, c.disableExprp->cloneTreePure(false)); + } + AstNodeExpr* guardp = nullptr; + for (AstNodeExpr* const cp : vtxp->m_throughoutConds) { + AstNodeExpr* const sampledp = sampled(cp->cloneTreePure(false)); + guardp = guardp ? static_cast(new AstLogAnd{c.flp, guardp, sampledp}) + : sampledp; + } + if (guardp) clearCondp = orExprs(c.flp, clearCondp, new AstLogNot{c.flp, guardp}); + if (clearCondp) { + // if (clear) ring <= '0; + AstConst* const zerop = new AstConst{c.flp, AstConst::DTyped{}, ringp->dtypep()}; + zerop->num().setAllBits0(); + updateBodyp = new AstIf{ + c.flp, clearCondp, + new AstAssignDly{c.flp, new AstVarRef{c.flp, ringp, VAccess::WRITE}, zerop}, + updateBodyp}; + } + // idx <= next_idx; + updateBodyp->addNext(new AstAssignDly{c.flp, + new AstVarRef{c.flp, idxp, VAccess::WRITE}, + nextRingIndex(c.flp, idxp, size)}); - AstNodeExpr* const donep = new AstLogOr{ - c.flp, killActive(c), new AstLogOr{c.flp, matchedNowp, counterAtEndp}}; - - AstAssignDly* const clearActivep - = new AstAssignDly{c.flp, new AstVarRef{c.flp, activep, VAccess::WRITE}, - new AstConst{c.flp, AstConst::BitFalse{}}}; - AstAdd* const addExprp - = new AstAdd{c.flp, new AstVarRef{c.flp, cntp, VAccess::READ}, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, 1u}}; - addExprp->dtypeFrom(cntp); - AstAssignDly* const incCountp - = new AstAssignDly{c.flp, new AstVarRef{c.flp, cntp, VAccess::WRITE}, addExprp}; - AstIf* const doneIfp = new AstIf{c.flp, donep, clearActivep, incCountp}; - - AstAssignDly* const setActivep - = new AstAssignDly{c.flp, new AstVarRef{c.flp, activep, VAccess::WRITE}, - new AstConst{c.flp, AstConst::BitTrue{}}}; - AstAssignDly* const resetCountp - = new AstAssignDly{c.flp, new AstVarRef{c.flp, cntp, VAccess::WRITE}, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, 0u}}; - setActivep->addNext(resetCountp); - AstIf* const startIfp - = new AstIf{c.flp, gateNotKill(c, incomingp), setActivep, nullptr}; - AstIf* const topIfp = new AstIf{c.flp, new AstVarRef{c.flp, activep, VAccess::READ}, - doneIfp, startIfp}; - - m_modp->addStmtsp( - new AstAlways{c.flp, VAlwaysKwd::ALWAYS, c.senTreep->cloneTree(false), topIfp}); + m_modp->addStmtsp(new AstAlways{c.flp, VAlwaysKwd::ALWAYS, + c.senTreep->cloneTree(false), updateBodyp}); } } @@ -1880,16 +1933,22 @@ class SvaNfaLowering final { outPerMidSrcsp->push_back(perMidp); } - if (tep->fromVtxp()->m_isCounter) { + if (tep->fromVtxp()->m_delayRingSize && !tep->fromVtxp()->m_isFixedDelayRing) { sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp->cloneTreePure(false)); - AstNodeExpr* const atEndp = new AstEq{ - c.flp, - new AstVarRef{c.flp, c.vtx[fi]->datap()->counterCountVarp, VAccess::READ}, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, - static_cast(tep->fromVtxp()->m_counterMax)}}; - AstNodeExpr* const expireContribp = new AstLogAnd{c.flp, srcSigp, atEndp}; + AstVar* const ringp = c.vtx[fi]->datap()->delayRingVarp; + AstVar* const idxp = c.vtx[fi]->datap()->delayRingIdxVarp; + const uint32_t size = static_cast(c.vtx[fi]->m_delayRingSize); + // reject |= ring[next_idx] && final_condition; + AstNodeExpr* expireContribp + = delayRingBit(c.flp, ringp, nextRingIndex(c.flp, idxp, size)); + expireContribp = andCond(c.flp, expireContribp, tep->m_condp); + if (snapshotOkp) { + expireContribp + = new AstLogAnd{c.flp, expireContribp, snapshotOkp->cloneTreePure(false)}; + } sigs.rejectBasep = orExprs(c.flp, sigs.rejectBasep, expireContribp); + VL_DO_DANGLING(srcSigp->deleteTree(), srcSigp); } else if (tep->fromVtxp()->m_isUnbounded || tep->fromVtxp()->m_isAndCombiner) { sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp); } else { @@ -1913,6 +1972,10 @@ class SvaNfaLowering final { AstNodeExpr* stateExprp = nullptr; if (c.vtx[i]->datap()->stateVarp) { stateExprp = new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::READ}; + } else if (c.vtx[i]->datap()->delayRingVarp && c.vtx[i]->m_isFixedDelayRing) { + // fixed_chain_active = |ring; + stateExprp = new AstRedOr{ + c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->delayRingVarp, VAccess::READ}}; } else { UASSERT_OBJ(c.vtx[i]->datap()->stateSigp, c.vtx[i], "Throughout-conds vertex missing state representation"); @@ -2027,10 +2090,18 @@ class SvaNfaLowering final { if (c.vtx[i]->datap()->stateVarp) { c.vtx[i]->datap()->stateSigp = new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::READ}; - } else if (c.vtx[i]->datap()->counterActiveVarp) { - // Counter window is always [0, m_counterMax]; see buildSExpr. - c.vtx[i]->datap()->stateSigp - = new AstVarRef{c.flp, c.vtx[i]->datap()->counterActiveVarp, VAccess::READ}; + } else if (c.vtx[i]->datap()->delayRingVarp) { + if (c.vtx[i]->m_isFixedDelayRing) { + // state = ring[idx]; + c.vtx[i]->datap()->stateSigp = delayRingBit( + c.flp, c.vtx[i]->datap()->delayRingVarp, + new AstVarRef{c.flp, c.vtx[i]->datap()->delayRingIdxVarp, VAccess::READ}); + } else { + // state = |ring; + c.vtx[i]->datap()->stateSigp = new AstRedOr{ + c.flp, + new AstVarRef{c.flp, c.vtx[i]->datap()->delayRingVarp, VAccess::READ}}; + } } } // Fixed-point propagation along zero-delay (Link) edges. @@ -2247,18 +2318,22 @@ public: vtx[i]->datap()->doneRVarp = rp; continue; } - if (vtx[i]->m_isCounter) { - const std::string base = baseName + "__c" + std::to_string(i); - AstVar* const activep = new AstVar{flp, VVarType::MODULETEMP, base + "_active", - m_modp->findBitDType()}; - activep->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(activep); - vtx[i]->datap()->counterActiveVarp = activep; - AstVar* const cntp - = new AstVar{flp, VVarType::MODULETEMP, base + "_cnt", u32DTypep}; - cntp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(cntp); - vtx[i]->datap()->counterCountVarp = cntp; + if (vtx[i]->m_delayRingSize) { + const std::string base = baseName + "__d" + std::to_string(i); + // bit [size-1:0] ring; + AstNodeDType* const ringDTypep = m_modp->findBitDType( + vtx[i]->m_delayRingSize, vtx[i]->m_delayRingSize, VSigning::UNSIGNED); + AstVar* const ringp + = new AstVar{flp, VVarType::MODULETEMP, base + "_ring", ringDTypep}; + ringp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(ringp); + vtx[i]->datap()->delayRingVarp = ringp; + // int unsigned idx; + AstVar* const idxp + = new AstVar{flp, VVarType::MODULETEMP, base + "_idx", u32DTypep}; + idxp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(idxp); + vtx[i]->datap()->delayRingIdxVarp = idxp; continue; } if (!vtx[i]->datap()->needsReg) continue; @@ -2279,9 +2354,9 @@ public: // Phase 1: Resolve combinational Links via fixed-point propagation. resolveLinks(c, triggerExprp); - // Phase 2/2b/2c: Emit NBA state-update, counter FSM, and SAnd done-latch logic. + // Phase 2/2b/2c: Emit NBA state-update, delay-ring, and SAnd done-latch logic. emitStateRegisterNba(c); - emitCounterFsmNba(c); + emitDelayRingNba(c); emitAndCombinerDoneLatchNba(c); emitKillAckNba(c); @@ -2876,11 +2951,17 @@ class AssertNfaVisitor final : public VNVisitor { bool senTreeOwned = false; // True if we created senTreep locally AstPropSpec* const propSpecp = VN_CAST(assertp->propp(), PropSpec); UASSERT_OBJ(propSpecp, assertp, "Concurrent assertion must have PropSpec"); + AstCover* const coverp = VN_CAST(assertp, Cover); + const bool isCover = coverp != nullptr; + const bool isCoverSeq = coverp && coverp->isCoverSeq(); + // A sequence event control is not an assertion directive; no default + // disable iff, no assertion control + const bool isSeqEvent = coverp && coverp->isSeqEvent(); // Inherit module defaults (IEEE 14.12, 16.15) when assertion has none. if (!propSpecp->sensesp() && m_defaultClockingp) { propSpecp->sensesp(m_defaultClockingp->sensesp()->cloneTree(true)); } - if (!propSpecp->disablep() && m_defaultDisablep) { + if (!propSpecp->disablep() && m_defaultDisablep && !isSeqEvent) { propSpecp->disablep(m_defaultDisablep->condp()->cloneTreePure(true)); } if (!senTreep && propSpecp->sensesp()) { @@ -2892,12 +2973,9 @@ class AssertNfaVisitor final : public VNVisitor { if (!senTreep) return; FileLine* const flp = assertp->fileline(); - const bool isCover = VN_IS(assertp, Cover); - AstCover* const coverp = VN_CAST(assertp, Cover); - const bool isCoverSeq = coverp && coverp->isCoverSeq(); SvaGraph graph; - SvaNfaBuilder builder{graph, m_modp, m_propTempNames, isCoverSeq}; + SvaNfaBuilder builder{graph, m_modp, m_propTempNames, isCoverSeq, isSeqEvent}; const BuildResult result = buildAssertionGraph(builder, graph, seqBodyp, parts, flp); if (result.valid()) wireMatchAndMidSources(graph, result, flp); @@ -2939,13 +3017,16 @@ class AssertNfaVisitor final : public VNVisitor { std::vector perMidSrcs; AstNodeExpr* const alwaysTriggerp - = assertOnCond(flp, assertp->userType(), assertp->directive()); + = isSeqEvent ? new AstConst{flp, AstConst::BitTrue{}} + : assertOnCond(flp, assertp->userType(), assertp->directive()); AstNodeExpr* const outputExprp = m_loweringp->lower( flp, graph, alwaysTriggerp, senTreep, result.finalCondp, isCover, disableExprp ? disableExprp->cloneTreePure(false) : nullptr, negated, needMatch ? &matchExprp : nullptr, disableCntVarp, snapshotVarp, needPerSrcFail ? &requiredStepSrcs : nullptr, isCoverSeq ? &perMidSrcs : nullptr, - assertp->userType(), assertp->directive()); + isSeqEvent ? VAssertType{VAssertType::INTERNAL} : assertp->userType(), + isSeqEvent ? VAssertDirectiveType{VAssertDirectiveType::INTERNAL} + : assertp->directive()); AstSenTree* const perSrcSenTreep = (requiredStepSrcs.size() >= 2) ? senTreep->cloneTree(false) : nullptr; @@ -2999,6 +3080,8 @@ class AssertNfaVisitor final : public VNVisitor { } UINFO(4, "NFA converted assertion at " << flp << endl); + + if (dumpGraphLevel() >= 6) graph.m_graph.dumpDotFilePrefixed("assert-nfa"); } // VISITORS diff --git a/src/V3AssertPre.cpp b/src/V3AssertPre.cpp index f45df83f3..d9332ff59 100644 --- a/src/V3AssertPre.cpp +++ b/src/V3AssertPre.cpp @@ -1466,7 +1466,9 @@ private: iterateAndNextNull(nodep->sensesp()); if (m_senip && m_senip != nodep->sensesp()) nodep->v3warn(E_UNSUPPORTED, "Unsupported: Only one PSL clock allowed per assertion"); - if (!nodep->disablep() && m_defaultDisablep) { + const AstCover* const coverp = VN_CAST(nodep->backp(), Cover); + const bool seqEvent = coverp && coverp->isSeqEvent(); + if (!nodep->disablep() && m_defaultDisablep && !seqEvent) { nodep->disablep(m_defaultDisablep->condp()->cloneTreePure(true)); } m_disablep = nodep->disablep(); diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index 3c25a9992..d2bbf21e4 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -94,6 +94,7 @@ class AstNodeFTask VL_NOT_FINAL : public AstNode { // @astgen op4 := scopeNamep : Optional[AstScopeName] string m_name; // Name of task string m_cname; // Name of task if DPI import + string m_dpiCDecl; // Custom DPI-C function declaration string m_ifacePortName; // Interface port name for out-of-block definition (IEEE 25.8) uint64_t m_dpiOpenParent = 0; // DPI import open array, if !=0, how many callees bool m_taskPublic : 1; // Public task @@ -199,6 +200,9 @@ public: void dpiOpenChild(bool flag) { m_dpiOpenChild = flag; } bool dpiTask() const { return m_dpiTask; } void dpiTask(bool flag) { m_dpiTask = flag; } + bool dpiCDeclOverride() const { return !m_dpiCDecl.empty(); } + const string& dpiCDecl() const { return m_dpiCDecl; } + void dpiCDecl(const string& cDecl) { m_dpiCDecl = cDecl; } bool isConstructor() const { return m_isConstructor; } void isConstructor(bool flag) { m_isConstructor = flag; } bool isHideLocal() const { return m_isHideLocal; } @@ -511,6 +515,7 @@ class AstCFunc final : public AstNode { string m_rtnType; // void, bool, or other return type string m_argTypes; // Argument types string m_ifdef; // #ifdef symbol around this function + string m_cDecl; // Custom DPI-C function declaration VBoolOrUnknown m_isConst; // Function is declared const (*this not changed) bool m_isStatic : 1; // Function is static (no need for a 'this' pointer) bool m_isTrace : 1; // Function is related to tracing @@ -640,6 +645,9 @@ public: void dpiImportPrototype(bool flag) { m_dpiImportPrototype = flag; } bool dpiImportWrapper() const { return m_dpiImportWrapper; } void dpiImportWrapper(bool flag) { m_dpiImportWrapper = flag; } + bool dpiCDeclOverride() const { return !m_cDecl.empty(); } + const string& dpiCDecl() const { return m_cDecl; } + void dpiCDecl(const string& cDecl) { m_cDecl = cDecl; } bool isCoroutine() const { return m_rtnType == "VlCoroutine"; } void recursive(bool flag) { m_recursive = flag; } bool recursive() const { return m_recursive; } diff --git a/src/V3AstNodeStmt.h b/src/V3AstNodeStmt.h index 25de3ad08..fbbceaec7 100644 --- a/src/V3AstNodeStmt.h +++ b/src/V3AstNodeStmt.h @@ -1612,6 +1612,8 @@ class AstCover final : public AstNodeCoverOrAssert { // @astgen op3 := coverincsp: List[AstNode] // Coverage node bool m_isCoverSeq = false; // 'cover sequence' (IEEE 1800-2023 16.14.3): fires per // end-of-match, not per property success + bool m_isSeqEvent = false; // Synthesized for a sequence used as an event control + // (IEEE 1800-2023 9.4.2.4) public: ASTGEN_MEMBERS_AstCover; AstCover(FileLine* fl, AstNode* propp, AstNode* stmtsp, VAssertType type, @@ -1622,6 +1624,8 @@ public: void dumpJson(std::ostream& str) const override; bool isCoverSeq() const { return m_isCoverSeq; } void isCoverSeq(bool flag) { m_isCoverSeq = flag; } + bool isSeqEvent() const { return m_isSeqEvent; } + void isSeqEvent(bool flag) { m_isSeqEvent = flag; } }; class AstRestrict final : public AstNodeCoverOrAssert { public: diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index aba8a9d0f..26126e5fd 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -2241,9 +2241,11 @@ void AstNodeCoverOrAssert::dumpJson(std::ostream& str) const { void AstCover::dump(std::ostream& str) const { this->AstNodeCoverOrAssert::dump(str); if (isCoverSeq()) str << " [COVERSEQ]"; + if (isSeqEvent()) str << " [SEQEVENT]"; } void AstCover::dumpJson(std::ostream& str) const { dumpJsonBoolFuncIf(str, isCoverSeq); + dumpJsonBoolFuncIf(str, isSeqEvent); this->AstNodeCoverOrAssert::dumpJson(str); } void AstClocking::dump(std::ostream& str) const { diff --git a/src/V3Coverage.cpp b/src/V3Coverage.cpp index 9db38cb6e..d34b3c152 100644 --- a/src/V3Coverage.cpp +++ b/src/V3Coverage.cpp @@ -385,8 +385,7 @@ class CoverageVisitor final : public VNVisitor { VL_RESTORER(m_state); VL_RESTORER(m_exprStmtsp); VL_RESTORER(m_inToggleOff); - // skip properties for expresison coverage - if (!VN_IS(nodep, Property)) m_exprStmtsp = nodep; + m_exprStmtsp = nodep; m_inToggleOff = true; createHandle(nodep); iterateChildren(nodep); @@ -745,6 +744,11 @@ class CoverageVisitor final : public VNVisitor { newCoverInc(nodep->fileline(), declp, m_beginHier + "_vlCoverageUserTrace")); } } + void visit(AstPropSpec* nodep) override { + VL_RESTORER(m_exprStmtsp); + m_exprStmtsp = nullptr; + iterateChildren(nodep); + } void visit(AstStop* nodep) override { UINFO(4, " STOP: " << nodep); m_state.m_on = false; diff --git a/src/V3Covergroup.cpp b/src/V3Covergroup.cpp index f243b0c33..90bae71a2 100644 --- a/src/V3Covergroup.cpp +++ b/src/V3Covergroup.cpp @@ -1692,32 +1692,37 @@ class FunctionalCoverageVisitor final : public VNVisitor { } // VISITORS - AstNode* findEnclosingMemberRef(AstClass* cgClassp) { - // An embedded covergroup is lowered into a sibling AstClass that has no handle to - // the enclosing object. A coverpoint/iff/cross expression that references a - // (non-static) member of the enclosing class therefore emits C++ that accesses the - // member as if it were static ("invalid use of non-static data member"). Detect - // such references so the caller can skip lowering with a clean warning instead of - // producing uncompilable code. Returns the first offending node, or nullptr. - // Collect the covergroup class's own member variables (sample/constructor args); - // references to those are legitimate. + AstNode* findUnsupportedCoverpointRef(AstClass* cgClassp) { + // An embedded covergroup is lowered into a sibling AstClass that (currently) has + // no handle to the enclosing object. Identify refs to the containing context + // or a formal param and flag as unsupported std::set ownVars; for (AstNode* itemp = cgClassp->membersp(); itemp; itemp = itemp->nextp()) { if (const AstVar* const varp = VN_CAST(itemp, Var)) ownVars.insert(varp); } + // Flag non-static enclosing-class members reached without a handle as unsupported AstNode* offenderp = nullptr; - const auto scan = [&](AstNode* rootp) { + const auto scanEnclosing = [&](AstNode* rootp) { rootp->foreach([&](AstVarRef* refp) { if (offenderp) return; - const AstVar* const varp = refp->varp(); // Always set post-LinkDot - // A member of another class (the enclosing class) reached with no handle. - // Members of the covergroup class itself (sample/constructor args) are - // legitimate and excluded via ownVars. + const AstVar* const varp = refp->varp(); if (varp->isClassMember() && !ownVars.count(varp)) offenderp = refp; }); }; - for (AstCoverpoint* cpp : m_coverpoints) scan(cpp); - for (AstCoverCross* crossp : m_coverCrosses) scan(crossp); + for (AstCoverpoint* cpp : m_coverpoints) scanEnclosing(cpp); + for (AstCoverCross* crossp : m_coverCrosses) scanEnclosing(crossp); + if (offenderp) return offenderp; + // Flag references to covergroup formal parameters as currently unsupported + const auto scanHandleDeref = [&](AstNode* rootp) { + if (!rootp) return; + rootp->foreach([&](AstMemberSel* selp) { + if (!offenderp) offenderp = selp; + }); + }; + for (AstCoverpoint* cpp : m_coverpoints) { + scanHandleDeref(cpp->exprp()); + scanHandleDeref(cpp->iffp()); + } return offenderp; } @@ -1801,16 +1806,16 @@ class FunctionalCoverageVisitor final : public VNVisitor { iterateChildren(nodep); - // Option B safety net for embedded covergroups: if a coverpoint/iff/cross - // references a member of the enclosing class, lowering would emit uncompilable - // C++ (no handle to the enclosing instance). Skip this covergroup with a clean - // warning rather than crashing the C++ compile. (Full support - an enclosing - // back-pointer - is the planned follow-up.) - if (AstNode* const offenderp = findEnclosingMemberRef(nodep)) { + // Identify embedded covergroup refs to enclosing class members or + // covergroup formal parameters and flag as currently unsupported. + if (AstNode* const offenderp = findUnsupportedCoverpointRef(nodep)) { + const bool viaHandle = VN_IS(offenderp, MemberSel); offenderp->v3warn(COVERIGN, - "Unsupported: 'covergroup' coverpoint referencing enclosing " - "class member; ignoring covergroup " - << nodep->prettyNameQ()); + "Unsupported: 'covergroup' coverpoint " + << (viaHandle ? "dereferencing a class handle member " + "(parameterized covergroup)" + : "referencing enclosing class member") + << "; ignoring covergroup " << nodep->prettyNameQ()); for (AstCoverpoint* cpp : m_coverpoints) { VL_DO_DANGLING(pushDeletep(cpp->unlinkFrBack()), cpp); } diff --git a/src/V3Dfg.cpp b/src/V3Dfg.cpp index 2fd5afb95..b729e841a 100644 --- a/src/V3Dfg.cpp +++ b/src/V3Dfg.cpp @@ -34,180 +34,6 @@ DfgGraph::~DfgGraph() { forEachVertex([&](DfgVertex& vtx) { vtx.unlinkDelete(*this); }); } -std::unique_ptr DfgGraph::clone() const { - // Create the new graph - DfgGraph* const clonep = new DfgGraph{name()}; - - // Map from original vertex to clone - std::unordered_map vtxp2clonep(size() * 2); - - // Clone constVertices - for (const DfgConst& vtx : m_constVertices) { - DfgConst* const cp = new DfgConst{*clonep, vtx.fileline(), vtx.num()}; - vtxp2clonep.emplace(&vtx, cp); - } - // Clone variable vertices - for (const DfgVertexVar& vtx : m_varVertices) { - const DfgVertexVar* const vp = vtx.as(); - DfgVertexVar* cp = nullptr; - - switch (vtx.type()) { - case VDfgType::VarArray: { - cp = new DfgVarArray{*clonep, vp->vscp()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::VarPacked: { - cp = new DfgVarPacked{*clonep, vp->vscp()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - default: { - vtx.v3fatalSrc("Unhandled variable vertex type: " + vtx.typeName()); - VL_UNREACHABLE; - break; - } - } - - if (AstVarScope* const tmpForp = vp->tmpForp()) cp->tmpForp(tmpForp); - } - // Clone ast reference vertices - for (const DfgVertexAst& vtx : m_astVertices) { // LCOV_EXCL_START - switch (vtx.type()) { - case VDfgType::AstRd: { - const DfgAstRd* const vp = vtx.as(); - DfgAstRd* const cp = new DfgAstRd{*clonep, vp->exprp(), vp->inSenItem(), vp->inLoop()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - default: { - vtx.v3fatalSrc("Unhandled ast reference vertex type: " + vtx.typeName()); - VL_UNREACHABLE; - break; - } - } - } // LCOV_EXCL_STOP - // Clone operation vertices - for (const DfgVertex& vtx : m_opVertices) { - switch (vtx.type()) { -#include "V3Dfg__gen_clone_cases.h" // From ./astgen - case VDfgType::CReset: { // LCOV_EXCL_START - No algorithm actually hits this today - DfgCReset* const cp = new DfgCReset{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } // LCOV_EXCL_STOP - case VDfgType::MatchMasked: { - DfgMatchMasked* const cp = new DfgMatchMasked{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::Sel: { - DfgSel* const cp = new DfgSel{*clonep, vtx.fileline(), vtx.dtype()}; - cp->lsb(vtx.as()->lsb()); - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::Rep: { - DfgRep* const cp = new DfgRep{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::UnitArray: { - DfgUnitArray* const cp = new DfgUnitArray{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::Mux: { - DfgMux* const cp = new DfgMux{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::SpliceArray: { - DfgSpliceArray* const cp = new DfgSpliceArray{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::SplicePacked: { - DfgSplicePacked* const cp = new DfgSplicePacked{*clonep, vtx.fileline(), vtx.dtype()}; - vtxp2clonep.emplace(&vtx, cp); - break; - } - case VDfgType::Logic: { - vtx.v3fatalSrc("DfgLogic cannot be cloned"); - VL_UNREACHABLE; - break; - } - case VDfgType::Unresolved: { - vtx.v3fatalSrc("DfgUnresolved cannot be cloned"); - VL_UNREACHABLE; - break; - } - case VDfgType::AstRd: // LCOV_EXCL_START - case VDfgType::Const: - case VDfgType::VarArray: - case VDfgType::VarPacked: { - vtx.v3fatalSrc("Vertex should have been handled above: " + vtx.typeName()); - VL_UNREACHABLE; - break; - } // LCOV_EXCL_STOP - } - } - UASSERT(size() == clonep->size(), "Size of clone should be the same"); - - // Constants have no inputs - // Hook up inputs of cloned variables - for (const DfgVertexVar& vtx : m_varVertices) { - DfgVertexVar* const cp = vtxp2clonep.at(&vtx)->as(); - if (const DfgVertex* const srcp = vtx.srcp()) cp->srcp(vtxp2clonep.at(srcp)); - if (const DfgVertex* const defp = vtx.defaultp()) cp->defaultp(vtxp2clonep.at(defp)); - } - // Hook up inputs of cloned ast references - for (const DfgVertexAst& vtx : m_astVertices) { // LCOV_EXCL_START - switch (vtx.type()) { - case VDfgType::AstRd: { - const DfgAstRd* const vp = vtx.as(); - DfgAstRd* const cp = vtxp2clonep.at(&vtx)->as(); - if (const DfgVertex* const srcp = vp->srcp()) cp->srcp(vtxp2clonep.at(srcp)); - break; - } - default: { - vtx.v3fatalSrc("Unhandled DfgVertexAst sub type: " + vtx.typeName()); - VL_UNREACHABLE; - break; - } - } - } // LCOV_EXCL_STOP - // Hook up inputs of cloned operation vertices - for (const DfgVertex& vtx : m_opVertices) { - if (vtx.is()) { - switch (vtx.type()) { - case VDfgType::SpliceArray: - case VDfgType::SplicePacked: { - const DfgVertexSplice* const vp = vtx.as(); - DfgVertexSplice* const cp = vtxp2clonep.at(vp)->as(); - vp->foreachDriver([&](const DfgVertex& src, uint32_t lo, FileLine* flp) { - cp->addDriver(vtxp2clonep.at(&src), lo, flp); - return false; - }); - break; - } - default: { - vtx.v3fatalSrc("Unhandled DfgVertexVariadic sub type: " + vtx.typeName()); - VL_UNREACHABLE; - break; - } - } - } else { - DfgVertex* const cp = vtxp2clonep.at(&vtx); - for (size_t i = 0; i < vtx.nInputs(); ++i) { - cp->inputp(i, vtxp2clonep.at(vtx.inputp(i))); - } - } - } - - return std::unique_ptr{clonep}; -} - void DfgGraph::mergeGraphs(std::vector>&& otherps) { if (otherps.empty()) return; diff --git a/src/V3Dfg.h b/src/V3Dfg.h index d80be4d84..c66d2d1a6 100644 --- a/src/V3Dfg.h +++ b/src/V3Dfg.h @@ -487,9 +487,6 @@ public: for (const DfgVertex& vtx : m_opVertices) f(vtx); } - // Return an identical, independent copy of this graph. Vertex and edge order might differ. - std::unique_ptr clone() const VL_MT_DISABLED; - // Merge contents of other graphs into this graph. Deletes the other graphs. // DfgVertexVar instances representing the same Ast variable are unified. void mergeGraphs(std::vector>&& otherps) VL_MT_DISABLED; diff --git a/src/V3DfgBreakCycles.cpp b/src/V3DfgBreakCycles.cpp index c1e3b28c5..9839beab7 100644 --- a/src/V3DfgBreakCycles.cpp +++ b/src/V3DfgBreakCycles.cpp @@ -1583,35 +1583,18 @@ public: } }; -std::pair, bool> // -breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) { +bool breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) { // Shorthand for dumping graph at given dump level const auto dump = [&](int level, const DfgGraph& dfg, const std::string& name) { if (dumpDfgLevel() >= level) dfg.dumpDotFilePrefixed("breakCycles-" + name); }; - // Can't do much with trivial things ('a = a' or 'a[1] = a[0]'), so bail - if (dfg.size() <= 2) { - UINFO(7, "Graph is trivial"); - dump(9, dfg, "trivial"); - ++ctx.m_breakCyclesContext.m_nTrivial; - return {nullptr, false}; - } - // AstNetlist/AstNodeModule user2 used as sequence numbers for temporaries const VNUser2InUse user2InUse; // Show input for debugging dump(7, dfg, "input"); - // We might fail to make any improvements, so first create a clone of the - // graph. This is what we will be working on, and return if successful. - // Do not touch the input graph. - std::unique_ptr resultp = dfg.clone(); - // Just shorthand for code below - DfgGraph& res = *resultp; - dump(9, res, "clone"); - // How many improvements have we made size_t nImprovements = 0; size_t prevNImprovements; @@ -1619,16 +1602,16 @@ breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) { // Iterate while an improvement can be made and the graph is still cyclic do { // Compute SCCs - SccInfo sccInfo{res}; + SccInfo sccInfo{dfg}; // Fix up independent ranges in vertices UINFO(9, "New iteration after " << nImprovements << " improvements"); prevNImprovements = nImprovements; - const size_t nFixed = FixUp::apply(res, sccInfo); + const size_t nFixed = FixUp::apply(dfg, sccInfo); if (nFixed) { nImprovements += nFixed; - ctx.m_breakCyclesContext.m_nImprovements += nFixed; - dump(9, res, "FixUp"); + ctx.m_nImprovements += nFixed; + dump(9, dfg, "FixUp"); } // Validate SccInfo if in debug mode @@ -1637,42 +1620,38 @@ breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) { // Congrats if it has become acyclic if (!sccInfo.isCyclic()) { UINFO(7, "Graph became acyclic after " << nImprovements << " improvements"); - dump(7, res, "result-acyclic"); - ++ctx.m_breakCyclesContext.m_nFixed; - return {std::move(resultp), true}; + dump(7, dfg, "result-acyclic"); + ++ctx.m_nFixed; + return true; } } while (nImprovements != prevNImprovements); + // Debug dump if (dumpDfgLevel() >= 9) { - const SccInfo sccInfo{res}; - res.dumpDotFilePrefixed("breakCycles-remaining", [&](const DfgVertex& vtx) { + const SccInfo sccInfo{dfg}; + dfg.dumpDotFilePrefixed("breakCycles-remaining", [&](const DfgVertex& vtx) { return sccInfo.get(vtx); // }); } - // If an improvement was made, return the still cyclic improved graph + // Accounting if (nImprovements) { UINFO(7, "Graph was improved " << nImprovements << " times"); - dump(7, res, "result-improved"); - ++ctx.m_breakCyclesContext.m_nImproved; - return {std::move(resultp), false}; + dump(7, dfg, "result-improved"); + ++ctx.m_nImproved; + } else { + UINFO(7, "Graph NOT improved"); + dump(7, dfg, "result-original"); + ++ctx.m_nUnchanged; } - - // No improvement was made - UINFO(7, "Graph NOT improved"); - dump(7, res, "result-original"); - ++ctx.m_breakCyclesContext.m_nUnchanged; - return {nullptr, false}; + return false; } } //namespace V3DfgBreakCycles -std::pair, bool> // -V3DfgPasses::breakCycles(const DfgGraph& dfg, V3DfgContext& ctx) { - auto pair = V3DfgBreakCycles::breakCycles(dfg, ctx); - if (pair.first) { - if (v3Global.opt.debugCheck()) V3DfgPasses::typeCheck(*pair.first); - V3DfgPasses::removeUnused(*pair.first); - } - return pair; +bool V3DfgPasses::breakCycles(DfgGraph& dfg, V3DfgContext& ctx) { + const bool res = V3DfgBreakCycles::breakCycles(dfg, ctx.m_breakCyclesContext); + if (v3Global.opt.debugCheck()) V3DfgPasses::typeCheck(dfg); + V3DfgPasses::removeUnused(dfg); + return res; } diff --git a/src/V3DfgContext.h b/src/V3DfgContext.h index 21e7af31a..ee34df5f6 100644 --- a/src/V3DfgContext.h +++ b/src/V3DfgContext.h @@ -100,9 +100,8 @@ class V3DfgBreakCyclesContext final : public V3DfgSubContext { public: // STATE VDouble0 m_nFixed; // Number of graphs that became acyclic - VDouble0 m_nImproved; // Number of graphs that were imporoved but still cyclic + VDouble0 m_nImproved; // Number of graphs that were improved but still cyclic VDouble0 m_nUnchanged; // Number of graphs that were left unchanged - VDouble0 m_nTrivial; // Number of graphs that were not changed VDouble0 m_nImprovements; // Number of changes made to graphs private: @@ -112,7 +111,6 @@ private: addStat("made acyclic", m_nFixed); addStat("improved", m_nImproved); addStat("left unchanged", m_nUnchanged); - addStat("trivial", m_nTrivial); addStat("changes applied", m_nImprovements); } }; diff --git a/src/V3DfgOptimizer.cpp b/src/V3DfgOptimizer.cpp index 07002049d..f18672598 100644 --- a/src/V3DfgOptimizer.cpp +++ b/src/V3DfgOptimizer.cpp @@ -126,19 +126,15 @@ class DataflowOptimize final { std::vector> madeAcyclicComponents; if (v3Global.opt.fDfgBreakCycles()) { for (auto it = cyclicComps.begin(); it != cyclicComps.end();) { - auto result = V3DfgPasses::breakCycles(**it, m_ctx); - if (!result.first) { - // No improvement, moving on. + const bool madeAcyclic = V3DfgPasses::breakCycles(**it, m_ctx); + // If not made acyclic, keep it in 'cyclicComps' + if (!madeAcyclic) { ++it; - } else if (!result.second) { - // Improved, but still cyclic. Replace the original cyclic component. - *it = std::move(result.first); - ++it; - } else { - // Result became acyclic. Move to madeAcyclicComponents, delete original. - madeAcyclicComponents.emplace_back(std::move(result.first)); - it = cyclicComps.erase(it); + continue; } + // Otherwise move to 'madeAcyclicComponents' + madeAcyclicComponents.emplace_back(std::move(*it)); + it = cyclicComps.erase(it); } } // Merge those that were made acyclic back to the graph, this enables optimizing more diff --git a/src/V3DfgPasses.h b/src/V3DfgPasses.h index d0d8e62d8..3392f0703 100644 --- a/src/V3DfgPasses.h +++ b/src/V3DfgPasses.h @@ -44,14 +44,9 @@ void synthesize(DfgGraph&, V3DfgContext&) VL_MT_DISABLED; // Remove redundant selects void removeSelects(DfgGraph& dfg, V3DfgRemoveSelectsContext& ctx) VL_MT_DISABLED; // Attempt to make the given cyclic graph into an acyclic, or "less cyclic" -// equivalent. If the returned pointer is null, then no improvement was -// possible on the input graph. Otherwise the returned graph is an improvement -// on the input graph, with at least some cycles eliminated. The returned -// graph is always independent of the original. If an imporoved graph is -// returned, then the returned 'bool' flag indicated if the returned graph is -// acyclic (flag 'true'), or still cyclic (flag 'false'). -std::pair, bool> // -breakCycles(const DfgGraph&, V3DfgContext&) VL_MT_DISABLED; +// equivalent. Genuine combinational cycles can exist, so this might be +// unsuccessful. Returns true if the graph became acyclic, false otherwise. +bool breakCycles(DfgGraph&, V3DfgContext&) VL_MT_DISABLED; // Construct binary to oneHot decoders void binToOneHot(DfgGraph&, V3DfgBinToOneHotContext&) VL_MT_DISABLED; // Common subexpression elimination diff --git a/src/V3EmitCBase.cpp b/src/V3EmitCBase.cpp index f1680a40a..2e93700a5 100644 --- a/src/V3EmitCBase.cpp +++ b/src/V3EmitCBase.cpp @@ -137,6 +137,10 @@ void EmitCBaseVisitorConst::emitCDefaultConstructor(const AstNodeModule* const m void EmitCBaseVisitorConst::emitCFuncHeader(const AstCFunc* funcp, const AstNodeModule* modp, bool withScope) { if (funcp->slow()) putns(funcp, "VL_ATTR_COLD "); + if (funcp->dpiCDeclOverride()) { + putns(funcp, funcp->dpiCDecl() + ";\n"); + return; + } if (!funcp->isDestructor()) { putns(funcp, funcp->rtnTypeVoid()); puts(" "); diff --git a/src/V3EmitCSyms.cpp b/src/V3EmitCSyms.cpp index bcf79c164..baf500103 100644 --- a/src/V3EmitCSyms.cpp +++ b/src/V3EmitCSyms.cpp @@ -1434,8 +1434,12 @@ void EmitCSyms::emitDpiHdr() { if (!firstImp++) puts("\n// DPI IMPORTS\n"); putsDecoration(nodep, "// DPI import" + sourceLoc + "\n"); } - putns(nodep, "extern " + nodep->rtnTypeVoid() + " " + nodep->nameProtect() + "(" - + cFuncArgs(nodep) + ");\n"); + if (nodep->dpiCDeclOverride()) { + putns(nodep, "extern " + nodep->dpiCDecl() + ";\n"); + } else { + putns(nodep, "extern " + nodep->rtnTypeVoid() + " " + nodep->nameProtect() + "(" + + cFuncArgs(nodep) + ");\n"); + } } puts("\n"); diff --git a/src/V3PreProc.cpp b/src/V3PreProc.cpp index 67e2cf3f9..342797a72 100644 --- a/src/V3PreProc.cpp +++ b/src/V3PreProc.cpp @@ -545,6 +545,19 @@ void V3PreProcImp::comment(const string& text) { if (arg.size() && baseCmd == "public_flat_rw_on") baseCmd += "_sns"; // different cmd for applying sensitivity if (!printed) insertUnreadback("/*verilator " + baseCmd + "*/ " + arg + " /**/"); + } else if (VString::startsWith(cmd, "dpi_c_decl")) { + // "/*verilator dpi_c_decl foo(bar) */" -> "/*verilator dpi_c_decl*/ \"foo(bar)\"" + string baseCmd = cmd.substr(0, std::strlen("dpi_c_decl")); + string::size_type startOfArg = baseCmd.size(); + while (std::isspace(cmd[startOfArg])) startOfArg++; + string arg = cmd.substr(startOfArg); + if (arg.empty()) { + fileline()->v3warn( + BADVLTPRAGMA, + "No function declaration provided in /*verilator dpi_c_decl*/ meta-comment"); + return; + } + if (!printed) insertUnreadback("/*verilator " + baseCmd + "*/ \"" + arg + "\" /**/"); } else { if (!printed) insertUnreadback("/*verilator " + cmd + "*/"); } diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 9585d1b01..beb7b2469 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4499,39 +4499,59 @@ class RandomizeVisitor final : public VNVisitor { return buckets; } + static bool distBoundRefsRandVar(const AstNode* boundp) { + bool found = false; + boundp->foreach([&](const AstVarRef* vrefp) { + if (vrefp->varp()->rand().isRandomizable()) found = true; + }); + return found; + } + + // (distExpr >= lo) && (distExpr <= hi); signed comparisons for signed vars + static AstNodeExpr* newDistRangeMembership(AstDist* distp, const AstInsideRange* irp) { + FileLine* const fl = distp->fileline(); + const bool isSigned = distp->exprp()->isSigned(); + AstNodeExpr* const distExprGtep = distp->exprp()->cloneTreePure(false); + AstNodeExpr* const distExprLtep = distp->exprp()->cloneTreePure(false); + distExprGtep->user1(true); + distExprLtep->user1(true); + AstNodeExpr* const gep + = isSigned ? static_cast( + new AstGteS{fl, distExprGtep, irp->lhsp()->cloneTreePure(false)}) + : static_cast( + new AstGte{fl, distExprGtep, irp->lhsp()->cloneTreePure(false)}); + AstNodeExpr* const lep + = isSigned ? static_cast( + new AstLteS{fl, distExprLtep, irp->rhsp()->cloneTreePure(false)}) + : static_cast( + new AstLte{fl, distExprLtep, irp->rhsp()->cloneTreePure(false)}); + gep->user1(true); + lep->user1(true); + AstNodeExpr* const andp = new AstLogAnd{fl, gep, lep}; + andp->user1(true); + return andp; + } + + // Membership test for one bucket: a range comparison or an equality. + static AstNodeExpr* newDistMembershipTerm(AstDist* distp, AstNodeExpr* rangep) { + if (const AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) + return newDistRangeMembership(distp, irp); + FileLine* const fl = distp->fileline(); + AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); + eqExprp->user1(true); + AstNodeExpr* const eqp = new AstEq{fl, eqExprp, rangep->cloneTreePure(false)}; + eqp->user1(true); + return eqp; + } + // Hard constraint that the dist value stays inside the union of its ranges // (IEEE 1800-2023 18.5.3: values outside the set must never appear). AstConstraintExpr* buildDistMembership(AstDist* distp, const std::vector& buckets) { FileLine* const fl = distp->fileline(); - const bool isSigned = distp->exprp()->isSigned(); AstNodeExpr* unionExprp = nullptr; for (const auto& bucket : buckets) { - AstNodeExpr* memberp; - if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) { - AstNodeExpr* const gtExprp = distp->exprp()->cloneTreePure(false); - AstNodeExpr* const ltExprp = distp->exprp()->cloneTreePure(false); - gtExprp->user1(true); - ltExprp->user1(true); - AstNodeExpr* const gep - = isSigned ? static_cast( - new AstGteS{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}) - : static_cast( - new AstGte{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}); - AstNodeExpr* const lep - = isSigned ? static_cast( - new AstLteS{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}) - : static_cast( - new AstLte{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}); - gep->user1(true); - lep->user1(true); - memberp = new AstLogAnd{fl, gep, lep}; - } else { - AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); - eqExprp->user1(true); - memberp = new AstEq{fl, eqExprp, bucket.rangep->cloneTreePure(false)}; - } - memberp->user1(true); + AstNodeExpr* const memberp = newDistMembershipTerm(distp, bucket.rangep); if (!unionExprp) { unionExprp = memberp; } else { @@ -4601,24 +4621,7 @@ class RandomizeVisitor final : public VNVisitor { AstNodeExpr* unionExprp = nullptr; for (auto& bucket : buckets) { - AstNodeExpr* termp; - if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) { - AstNodeExpr* const gtExprp = distp->exprp()->cloneTreePure(false); - AstNodeExpr* const ltExprp = distp->exprp()->cloneTreePure(false); - gtExprp->user1(true); - ltExprp->user1(true); - AstGte* const gtep = new AstGte{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}; - gtep->user1(true); - AstLte* const ltep = new AstLte{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}; - ltep->user1(true); - termp = new AstLogAnd{fl, gtep, ltep}; - termp->user1(true); - } else { - AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); - eqExprp->user1(true); - termp = new AstEq{fl, eqExprp, bucket.rangep->cloneTreePure(false)}; - termp->user1(true); - } + AstNodeExpr* termp = newDistMembershipTerm(distp, bucket.rangep); // A weight that is zero only at runtime excludes the bucket's values too. bool runtimeWeight = false; bucket.weightExprp->foreach([&](const AstNodeVarRef*) { runtimeWeight = true; }); @@ -4777,7 +4780,13 @@ class RandomizeVisitor final : public VNVisitor { AstNode* chainp = nullptr; for (int i = static_cast(buckets.size()) - 1; i >= 0; --i) { AstNodeExpr* constraintExprp; - if (const AstInsideRange* const irp = VN_CAST(buckets[i].rangep, InsideRange)) { + const AstInsideRange* const irp = VN_CAST(buckets[i].rangep, InsideRange); + if (irp + && (distBoundRefsRandVar(irp->lhsp()) || distBoundRefsRandVar(irp->rhsp()))) { + // Bounds solved concurrently cannot pin a pre-solve value; softly + // prefer the symbolic range so the hard membership stays satisfiable + constraintExprp = newDistRangeMembership(distp, irp); + } else if (irp) { // Pick distExpr = lo + rand64() % (hi - lo + 1) for a uniform value in range AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); distExprCopyp->user1(true); diff --git a/src/V3Subst.cpp b/src/V3Subst.cpp index bdc62e5d5..8e995dfc6 100644 --- a/src/V3Subst.cpp +++ b/src/V3Subst.cpp @@ -183,6 +183,7 @@ class SubstValidVisitor final : public VNVisitorConst { } void visit(AstConst*) override {} // Accelerate + void visit(AstText*) override {} // CExpr/CStmt literal text has no variable dependencies void visit(AstNodeExpr* nodep) override { if (!m_valid) return; diff --git a/src/V3Task.cpp b/src/V3Task.cpp index 72e779d1e..28768e305 100644 --- a/src/V3Task.cpp +++ b/src/V3Task.cpp @@ -1060,6 +1060,7 @@ class TaskVisitor final : public VNVisitor { // Add DPI Import to top, since it's a global function m_topScopep->scopep()->addBlocksp(funcp); + funcp->dpiCDecl(nodep->dpiCDecl()); if (!makePortList(nodep, funcp)) return nullptr; return funcp; } diff --git a/src/V3Width.cpp b/src/V3Width.cpp index acc20248d..5eeaf2205 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -731,6 +731,17 @@ class WidthVisitor final : public VNVisitor { iterateCheckBool(nodep, "default disable iff condition", nodep->condp(), BOTH); } void visit(AstDelay* nodep) override { + if (nodep->isCycleDelay() && m_underSExpr) { + // Fold parameterized SVA cycle-delay bounds + userIterateAndNext(nodep->lhsp(), WidthVP{SELF, BOTH}.p()); + V3Const::constifyParamsNoWarnEdit(nodep->lhsp()); + if (nodep->rhsp() && !nodep->isUnbounded()) { + // Fold parametrized SVA cycle-delay max bound + userIterateAndNext(nodep->rhsp(), WidthVP{SELF, BOTH}.p()); + V3Const::constifyParamsNoWarnEdit(nodep->rhsp()); + } + return; + } if (AstNodeExpr* const fallDelayp = nodep->fallDelay()) { iterateCheckDelay(nodep, "delay", nodep->lhsp(), BOTH); iterateCheckDelay(nodep, "delay", fallDelayp, BOTH); @@ -3453,9 +3464,11 @@ class WidthVisitor final : public VNVisitor { for (AstNode *nextip, *itemp = nodep->itemsp(); itemp; itemp = nextip) { nextip = itemp->nextp(); itemp = VN_AS(itemp, DistItem)->rangep(); - // InsideRange will get replaced with Lte&Gte and finalized later - if (!VN_IS(itemp, InsideRange)) + if (VN_IS(itemp, InsideRange)) { + userIterate(itemp, WidthVP{subDTypep, FINAL}.p()); + } else { iterateCheck(nodep, "Dist Item", itemp, CONTEXT_DET, FINAL, subDTypep, EXTEND_EXP); + } } // Inside a constraint, V3Randomize handles dist lowering with proper weights, @@ -3530,10 +3543,12 @@ class WidthVisitor final : public VNVisitor { EXTEND_EXP); for (AstNode *nextip, *itemp = nodep->itemsp(); itemp; itemp = nextip) { nextip = itemp->nextp(); // iterate may cause the node to get replaced - // InsideRange will get replaced with Lte&Gte and finalized later - if (!VN_IS(itemp, InsideRange) && !itemp->dtypep()->isNonPackedArray()) + if (VN_IS(itemp, InsideRange)) { + userIterate(itemp, WidthVP{expDTypep, FINAL}.p()); + } else if (!itemp->dtypep()->isNonPackedArray()) { iterateCheck(nodep, "Inside Item", itemp, CONTEXT_DET, FINAL, expDTypep, EXTEND_EXP); + } } AstNodeExpr* exprp; @@ -3625,8 +3640,25 @@ class WidthVisitor final : public VNVisitor { V3Const::constifyEdit(nodep->lhsp()); // lhsp may change V3Const::constifyEdit(nodep->rhsp()); // rhsp may change } else { - userIterateAndNext(nodep->lhsp(), m_vup); - userIterateAndNext(nodep->rhsp(), m_vup); + if (m_vup->prelim()) { + userIterateAndNext(nodep->lhsp(), m_vup); + userIterateAndNext(nodep->rhsp(), m_vup); + } + if (m_vup->final()) { + AstNodeDType* const expDTypep = m_vup->dtypeOverridep(nodep->dtypep()); + // Warning waivers match visit_cmp_eq_gt on the lowered Gte/Lte + const int expWidth = expDTypep->width(); + const bool waiveLhs = expWidth == 32 + && !(expDTypep->isSigned() && nodep->lhsp()->isSigned()) + && expDTypep->widthMin() >= nodep->lhsp()->width(); + const bool waiveRhs = expWidth == 32 + && !(expDTypep->isSigned() && nodep->rhsp()->isSigned()) + && expWidth >= nodep->rhsp()->widthMin(); + iterateCheck(nodep, "Range LHS", nodep->lhsp(), CONTEXT_DET, FINAL, expDTypep, + EXTEND_EXP, !waiveLhs); + iterateCheck(nodep, "Range RHS", nodep->rhsp(), CONTEXT_DET, FINAL, expDTypep, + EXTEND_EXP, !waiveRhs); + } } nodep->dtypeFrom(nodep->lhsp()); } diff --git a/src/Verilator.cpp b/src/Verilator.cpp index b1f0a9af0..b694691dd 100644 --- a/src/Verilator.cpp +++ b/src/Verilator.cpp @@ -261,6 +261,7 @@ static void process() { // Assertion insertion // After we've added block coverage, but before other nasty transforms V3AssertCommon::collectDefaultDisable(v3Global.rootp()); + V3AssertCommon::lowerSequenceEvents(v3Global.rootp()); V3AssertNfa::assertNfaAll(v3Global.rootp()); // V3AssertProp removed: NFA subsumes multi-cycle property lowering. // Unsupported constructs fall through to V3AssertPre. diff --git a/src/astgen b/src/astgen index 75f68d5fb..7359833ff 100755 --- a/src/astgen +++ b/src/astgen @@ -1279,29 +1279,6 @@ def write_dfg_auto_classes(filename): fh.write("\n") -def write_dfg_clone_cases(filename): - with open_file(filename) as fh: - - def emitBlock(pattern, **fmt): - fh.write(textwrap.dedent(pattern).format(**fmt)) - - for node in DfgVertexList: - # Only generate code for automatically derived leaf nodes - if (node.file is not None) or not node.isLeaf: - continue - - emitBlock('''\ - case VDfgType::{t}: {{ - Dfg{t}* const cp = new Dfg{t}{{*clonep, vtx.fileline(), vtx.dtype()}}; - vtxp2clonep.emplace(&vtx, cp); - break; - }} - ''', - t=node.name, - s=node.superClass.name) - fh.write("\n") - - def write_dfg_ast_to_dfg(filename): with open_file(filename) as fh: for node in DfgVertexList: @@ -1499,7 +1476,6 @@ if Args.classes: write_type_tests("Dfg", DfgVertexList) write_dfg_macros("V3Dfg__gen_macros.h") write_dfg_auto_classes("V3Dfg__gen_auto_classes.h") - write_dfg_clone_cases("V3Dfg__gen_clone_cases.h") write_dfg_ast_to_dfg("V3Dfg__gen_ast_to_dfg.h") write_dfg_dfg_to_ast("V3Dfg__gen_dfg_to_ast.h") diff --git a/src/verilog.l b/src/verilog.l index 36058b0a6..d274148ff 100644 --- a/src/verilog.l +++ b/src/verilog.l @@ -825,6 +825,7 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5} "/*verilator coverage_block_off*/" { FL; return yVL_COVERAGE_BLOCK_OFF; } "/*verilator coverage_off*/" { FL_FWD; PARSEP->lexFileline()->coverageOn(false); FL_BRK; } "/*verilator coverage_on*/" { FL_FWD; PARSEP->lexFileline()->coverageOn(true); FL_BRK; } + "/*verilator dpi_c_decl*/" { FL; return yVL_DPI_C_DECL; } // The "foo(bar)" is converted by the preproc "/*verilator forceable*/" { FL; return yVL_FORCEABLE; } "/*verilator full_case*/" { FL; return yVL_FULL_CASE; } "/*verilator hier_block*/" { FL; return yVL_HIER_BLOCK; } diff --git a/src/verilog.y b/src/verilog.y index db145ea8e..378c4f402 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -780,6 +780,7 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"}) %token yVL_CLOCKER "/*verilator clocker*/" %token yVL_CLOCK_ENABLE "/*verilator clock_enable*/" %token yVL_COVERAGE_BLOCK_OFF "/*verilator coverage_block_off*/" +%token yVL_DPI_C_DECL "/*verilator dpi_c_decl*/" %token yVL_FORCEABLE "/*verilator forceable*/" %token yVL_FULL_CASE "/*verilator full_case*/" %token yVL_HIER_BLOCK "/*verilator hier_block*/" @@ -4928,6 +4929,14 @@ dpi_import_export: // ==IEEE: dpi_import_export $5->dpiPure($3 == iprop_PURE); $5->dpiImport(true); GRAMMARP->checkDpiVer($1, *$2); v3Global.dpi(true); } + | yIMPORT yaSTRING dpi_tf_import_propertyE dpi_importLabelE function_prototype yVL_DPI_C_DECL yaSTRING ';' + { $$ = $5; + if (*$4 != "") $5->cname(*$4); + $5->dpiContext($3 == iprop_CONTEXT); + $5->dpiPure($3 == iprop_PURE); + $5->dpiImport(true); + if (*$7 != "") $5->dpiCDecl(*$7); + GRAMMARP->checkDpiVer($1, *$2); v3Global.dpi(true); } | yIMPORT yaSTRING dpi_tf_import_propertyE dpi_importLabelE task_prototype ';' { $$ = $5; if (*$4 != "") $5->cname(*$4); diff --git a/test_regress/t/t_assert_inline_coverage.py b/test_regress/t/t_assert_inline_coverage.py new file mode 100755 index 000000000..fb117067b --- /dev/null +++ b/test_regress/t/t_assert_inline_coverage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--assert --cc --coverage']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_assert_inline_coverage.v b/test_regress/t/t_assert_inline_coverage.v new file mode 100644 index 000000000..08c1beece --- /dev/null +++ b/test_regress/t/t_assert_inline_coverage.v @@ -0,0 +1,43 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + integer cyc = 0; + logic rst_n = 0; + logic en = 0; + logic q = 0; + logic [7:0] cnt = 0; + + // Synchronous active-low reset driving runtime-varying signals, so the + // asserted and covered properties are not constant-folded away. + always_ff @(posedge clk) begin + rst_n <= (cyc >= 2); + en <= cyc[0]; + if (!rst_n) begin + q <= 1'b0; + cnt <= '0; + end else if (en) begin + q <= ~q; + cnt <= cnt + 8'd1; + end + end + + a : assert property (@(posedge clk) !rst_n |=> q == 1'b0); + + c : cover property (@(posedge clk) disable iff (!rst_n) en && cnt == $past(cnt)); + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 10) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + +endmodule diff --git a/test_regress/t/t_assert_seq_event.py b/test_regress/t/t_assert_seq_event.py new file mode 100755 index 000000000..e8cdbc78d --- /dev/null +++ b/test_regress/t/t_assert_seq_event.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=['--timing']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_assert_seq_event.v b/test_regress/t/t_assert_seq_event.v new file mode 100644 index 000000000..d82078cff --- /dev/null +++ b/test_regress/t/t_assert_seq_event.v @@ -0,0 +1,97 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +module t ( + input clk +); + int unsigned crc = 32'h1; + bit a, b, c; + bit a1, a2, a3, b1; + int cyc; + int seq_hits, seq_hits2, ref_hits, one_hits, dc_hits; + int rng_hits, rng_ref; + + // verilog_format: off // verible does not support clocking events inside sequence declarations + sequence seq; + @(posedge clk) a ##1 b ##1 c; + endsequence + + sequence seq_one; + @(posedge clk) a; + endsequence + + sequence seq_rng; + @(posedge clk) a ##[1:3] b; + endsequence + // verilog_format: on + + // seq_dc inherits the default clocking; counts must match seq + default clocking @(posedge clk); + endclocking + + sequence seq_dc; + a ##1 b ##1 c; + endsequence + + // ref_hits and rng_ref are independent shift-register oracles; + // simultaneous end points resume a blocked waiter once + initial forever begin + @seq; + seq_hits = seq_hits + 1; + end + initial forever begin + @seq; + seq_hits2 = seq_hits2 + 1; + end + initial forever begin + @seq_one; + one_hits = one_hits + 1; + end + initial forever begin + @seq_dc; + dc_hits = dc_hits + 1; + end + initial forever begin + @seq_rng; + rng_hits = rng_hits + 1; + end + + always @(posedge clk) begin + if (a2 && b1 && c) ref_hits = ref_hits + 1; + if (b && (a1 || a2 || a3)) rng_ref = rng_ref + 1; + a3 <= a2; + a2 <= a1; + a1 <= a; + b1 <= b; + end + + // a/b/c bit spacing exceeds the ##2 window to decorrelate the LFSR taps + always @(posedge clk) begin + cyc <= cyc + 1; + crc <= {crc[30:0], crc[31] ^ crc[21] ^ crc[1] ^ crc[0]}; + a <= crc[0]; + b <= crc[4]; + c <= crc[8]; + if (cyc == 60) $finish; + end + + // Counts read in final (Postponed) to avoid same-timestep races. + final begin + `checkd(seq_hits, 14); + `checkd(seq_hits2, 14); + `checkd(ref_hits, 14); + `checkd(one_hits, 30); + `checkd(dc_hits, 14); + `checkd(rng_hits, 26); + `checkd(rng_ref, 26); + $write("*-* All Finished *-*\n"); + end +endmodule diff --git a/test_regress/t/t_assert_seq_event_bad.out b/test_regress/t/t_assert_seq_event_bad.out new file mode 100644 index 000000000..7e24b4b9f --- /dev/null +++ b/test_regress/t/t_assert_seq_event_bad.out @@ -0,0 +1,6 @@ +%Error: t/t_assert_seq_event_bad.v:19:7: Arguments to a sequence used as an event control must be static (IEEE 1800-2023 9.4.2.4) + : ... note: In instance 't' + 19 | @(s_arg(x)); + | ^~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: Exiting due to diff --git a/test_regress/t/t_assert_seq_event_bad.py b/test_regress/t/t_assert_seq_event_bad.py new file mode 100755 index 000000000..f051d14b9 --- /dev/null +++ b/test_regress/t/t_assert_seq_event_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(expect_filename=test.golden_filename, verilator_flags2=['--timing'], fails=True) + +test.passes() diff --git a/test_regress/t/t_assert_seq_event_bad.v b/test_regress/t/t_assert_seq_event_bad.v new file mode 100644 index 000000000..d6621aa9d --- /dev/null +++ b/test_regress/t/t_assert_seq_event_bad.v @@ -0,0 +1,23 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + // verilog_format: off + sequence s_arg(x); + @(posedge clk) x; + endsequence + // verilog_format: on + + task automatic f; + bit x = 1; + @(s_arg(x)); + endtask + + initial f(); +endmodule diff --git a/test_regress/t/t_assert_seq_event_ctl.py b/test_regress/t/t_assert_seq_event_ctl.py new file mode 100755 index 000000000..e8cdbc78d --- /dev/null +++ b/test_regress/t/t_assert_seq_event_ctl.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=['--timing']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_assert_seq_event_ctl.v b/test_regress/t/t_assert_seq_event_ctl.v new file mode 100644 index 000000000..ce9931e4b --- /dev/null +++ b/test_regress/t/t_assert_seq_event_ctl.v @@ -0,0 +1,73 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +module t ( + input clk +); + int unsigned crc = 32'h1; + bit a, b, rst; + bit a1; + int cyc; + int hits, ref_hits, one_hits, one_ref; + + // Neither the default disable iff nor $assertoff may suppress a sequence event control + default disable iff (rst); + + // verilog_format: off // verible does not support clocking events inside sequence declarations + sequence seq; + @(posedge clk) a ##1 b; + endsequence + + sequence seq_one; + @(posedge clk) 1; + endsequence + // verilog_format: on + + initial begin + $assertoff; + #300 $assertkill; + end + + initial forever begin + @seq; + hits = hits + 1; + end + initial forever begin + @seq_one; + one_hits = one_hits + 1; + end + + always @(posedge clk) begin + if (a1 && b) ref_hits = ref_hits + 1; + one_ref = one_ref + 1; + a1 <= a; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + crc <= {crc[30:0], crc[31] ^ crc[21] ^ crc[1] ^ crc[0]}; + a <= crc[0]; + b <= crc[4]; + rst <= crc[2]; + end + + always @(negedge clk) begin + if (cyc == 60) $finish; + end + + final begin + `checkd(hits, ref_hits); + `checkd(one_hits, one_ref); + `checkd(hits, 19); + `checkd(one_hits, 60); + $write("*-* All Finished *-*\n"); + end +endmodule diff --git a/test_regress/t/t_assert_seq_event_unsup.out b/test_regress/t/t_assert_seq_event_unsup.out new file mode 100644 index 000000000..61d38b9f7 --- /dev/null +++ b/test_regress/t/t_assert_seq_event_unsup.out @@ -0,0 +1,13 @@ +%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:18:5: Unsupported: non-edge clocking event on a sequence; use an edge such as @(posedge clk) + : ... note: In instance 't' + 18 | @(g) a ##1 b; + | ^ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:28:30: Unsupported: sequence used as an event control with a sequence operand of 'or' + 28 | @(posedge clk) (a ##1 b) or (a ##2 b); + | ^~ +%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:21:12: Unsupported: sequence referenced outside assertion property + : ... note: In instance 't' + 21 | sequence s_ref; + | ^~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_assert_seq_event_unsup.py b/test_regress/t/t_assert_seq_event_unsup.py new file mode 100755 index 000000000..f051d14b9 --- /dev/null +++ b/test_regress/t/t_assert_seq_event_unsup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(expect_filename=test.golden_filename, verilator_flags2=['--timing'], fails=True) + +test.passes() diff --git a/test_regress/t/t_assert_seq_event_unsup.v b/test_regress/t/t_assert_seq_event_unsup.v new file mode 100644 index 000000000..eda3b0772 --- /dev/null +++ b/test_regress/t/t_assert_seq_event_unsup.v @@ -0,0 +1,42 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + bit a, b; + logic g = 0; + + default clocking @(posedge clk); + endclocking + + // verilog_format: off + sequence s_nonedge; + @(g) a ##1 b; + endsequence + + sequence s_ref; + @(posedge clk) a; + endsequence + + // Legal but its endpoint topology is not buildable, so the wait could never + // resume; rejected rather than silently ignored. + sequence s_or; + @(posedge clk) (a ##1 b) or (a ##2 b); + endsequence + // verilog_format: on + + // Legal: p is never asserted, so s_ref stays referenced outside any + // assertion property, which is not yet supported. + property p; + s_ref; + endproperty + + initial begin + @s_nonedge; + @s_or; + end +endmodule diff --git a/test_regress/t/t_constraint_inside_impl_mixwidth.py b/test_regress/t/t_constraint_inside_impl_mixwidth.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_constraint_inside_impl_mixwidth.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_constraint_inside_impl_mixwidth.v b/test_regress/t/t_constraint_inside_impl_mixwidth.v new file mode 100644 index 000000000..25cd24ca9 --- /dev/null +++ b/test_regress/t/t_constraint_inside_impl_mixwidth.v @@ -0,0 +1,155 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +// verilator lint_off WIDTHEXPAND +class Impl; + rand bit [63:0] x, y; + rand bit [31:0] g; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + y != 0 -> x inside {[y - g : y]}; + } +endclass + +class Neg; // inside under logical-not + rand bit [63:0] x, y; + rand bit [31:0] g; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + y != 0 -> !(x inside {[y - g : y - 1]}); + } +endclass + +class LAnd; // inside as a logical-and operand + rand bit [63:0] x, y; + rand bit [31:0] g; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + (x inside {[y - g : y]}) && (x[0] == 1'b0); + } +endclass + +class Nest; // nested implication a -> (b -> inside) + rand bit [63:0] x, y; + rand bit [31:0] g; + rand bit a, b; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + a == 1; + b == 1; + a -> (b -> x inside {[y - g : y]}); + } +endclass + +class CondCtx; // inside as a ?: condition + rand bit [63:0] x, y; + rand bit [31:0] g; + rand bit s; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + (x inside {[y - g : y]}) ? (s == 1'b1) : (s == 1'b0); + } +endclass + +class Ctl; // all-32-bit control + rand bit [31:0] x, y, g; + constraint c { + g inside {[1 : 10]}; + y == 32'h100; + y != 0 -> x inside {[y - g : y]}; + } +endclass + +class DistRange; // mixed-width dist range bound + rand bit [63:0] x, y; + rand bit [31:0] g; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + x dist { + [y - g : y] := 1, + 5 := 1 + }; + } +endclass + +class Bare; // bare narrow variable as a bound + rand bit [63:0] x, y; + rand bit [31:0] g; + constraint c { + g inside {[1 : 10]}; + y == 64'h100; + y != 0 -> x inside {[g : y]}; + } +endclass + +module t; + Impl im; + Neg ng; + LAnd la; + Nest ne; + CondCtx cx; + Ctl ct; + DistRange dr; + Bare br; + int ok; + initial begin + im = new; + ng = new; + la = new; + ne = new; + cx = new; + ct = new; + dr = new; + br = new; + for (int i = 0; i < 20; ++i) begin + ok = im.randomize(); + `checkd(ok, 1); + if (im.x < (64'h100 - im.g) || im.x > 64'h100) `checkd(0, 1); + + ok = ng.randomize(); + `checkd(ok, 1); + if (ng.x >= (64'h100 - ng.g) && ng.x <= 64'hFF) `checkd(0, 1); + + ok = la.randomize(); + `checkd(ok, 1); + if (la.x < (64'h100 - la.g) || la.x > 64'h100 || la.x[0] !== 1'b0) `checkd(0, 1); + + ok = ne.randomize(); + `checkd(ok, 1); + if (ne.x < (64'h100 - ne.g) || ne.x > 64'h100) `checkd(0, 1); + + ok = cx.randomize(); + `checkd(ok, 1); + if (cx.s !== ((cx.x >= (64'h100 - cx.g)) && (cx.x <= 64'h100))) `checkd(0, 1); + + ok = ct.randomize(); + `checkd(ok, 1); + if (ct.x < (32'h100 - ct.g) || ct.x > 32'h100) `checkd(0, 1); + + ok = dr.randomize(); + `checkd(ok, 1); + if (dr.x != 5 && (dr.x < (64'h100 - dr.g) || dr.x > 64'h100)) `checkd(0, 1); + + ok = br.randomize(); + `checkd(ok, 1); + if (br.x < {32'h0, br.g} || br.x > 64'h100) `checkd(0, 1); + end + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule +// verilator lint_on WIDTHEXPAND diff --git a/test_regress/t/t_covergroup_embedded_unsup.out b/test_regress/t/t_covergroup_embedded_unsup.out index 93cf7a3a5..d74a490cf 100644 --- a/test_regress/t/t_covergroup_embedded_unsup.out +++ b/test_regress/t/t_covergroup_embedded_unsup.out @@ -1,7 +1,11 @@ -%Warning-COVERIGN: t/t_covergroup_embedded_unsup.v:27:34: Unsupported: 'covergroup' coverpoint referencing enclosing class member; ignoring covergroup '__vlAnonCG_cov_trans' +%Warning-COVERIGN: t/t_covergroup_embedded_unsup.v:23:34: Unsupported: 'covergroup' coverpoint referencing enclosing class member; ignoring covergroup '__vlAnonCG_cov_trans' : ... note: In instance 't' - 27 | trans_start_addr: coverpoint trans_collected.addr {option.auto_bin_max = 16;} + 23 | trans_start_addr: coverpoint trans_collected.addr {option.auto_bin_max = 16;} | ^~~~~~~~~~~~~~~ ... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Warning-COVERIGN: t/t_covergroup_embedded_unsup.v:46:23: Unsupported: 'covergroup' coverpoint dereferencing a class handle member (parameterized covergroup); ignoring covergroup '__vlAnonCG_cov_param' + : ... note: In instance 't' + 46 | cp: coverpoint st.test; + | ^~~~ %Error: Exiting due to diff --git a/test_regress/t/t_covergroup_embedded_unsup.v b/test_regress/t/t_covergroup_embedded_unsup.v index a35ffb840..8a81eb4ab 100644 --- a/test_regress/t/t_covergroup_embedded_unsup.v +++ b/test_regress/t/t_covergroup_embedded_unsup.v @@ -5,13 +5,9 @@ // SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 -// Test the graceful-degradation safety net for embedded covergroups (the dominant -// UVM pattern: a covergroup declared inside a class whose coverpoints reference the -// enclosing object's members). Such a covergroup is lowered into a sibling class -// with no handle to the enclosing instance, so emitting it would produce -// uncompilable C++ ("invalid use of non-static data member"). Until the enclosing -// back-pointer feature exists, Verilator must emit a clean COVERIGN warning and skip -// lowering the covergroup, rather than crashing the C++ compile. +// Test that two currently-unsupported coverpoint reference styles are properly flagged +// as COVIGN: references to containing-class members ; references to covergroup formal +// parameters class ubus_transfer; bit [15:0] addr; @@ -35,10 +31,34 @@ class ubus_master_monitor; endfunction endclass +class coverage_state; + bit [3:0] test; + bit [3:0] test2; +endclass + +class parameterized_monitor; + coverage_state cs; + + // Parameterized covergroup: the coverpoints dereference the class-handle argument 'st'. + // Two handle-dereferencing coverpoints ensure the safety net reports only the first + // offender (a second AstMemberSel is seen with the offender already latched). + covergroup cov_param(coverage_state st); + cp: coverpoint st.test; + cp2: coverpoint st.test2; + endgroup + + function new(); + cs = new; + cov_param = new(cs); + endfunction +endclass + module t; ubus_master_monitor m; + parameterized_monitor p; initial begin m = new; + p = new; $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_debug_emitv.out b/test_regress/t/t_debug_emitv.out index 8c0ad2245..5fcf5c7d7 100644 --- a/test_regress/t/t_debug_emitv.out +++ b/test_regress/t/t_debug_emitv.out @@ -1318,6 +1318,21 @@ module Vt_debug_emitv_sub; endfunction real r; endmodule +module Vt_debug_emitv_seq_event; + input logic clk; + bit a; + bit b; + bit c; + sequence sq; + @(posedge clk) a##1 b##1 c + endsequence + initial begin + begin + + ???? // EVENTCONTROL + @( sq())end + end +endmodule package Vt_debug_emitv_p; logic pkgvar; endpackage diff --git a/test_regress/t/t_debug_emitv.v b/test_regress/t/t_debug_emitv.v index 813d02ea9..7e474c305 100644 --- a/test_regress/t/t_debug_emitv.v +++ b/test_regress/t/t_debug_emitv.v @@ -138,6 +138,7 @@ module t (/*AUTOARG*/ endfunction sub sub(.*); + seq_event seq_event(.*); initial begin int other; @@ -443,6 +444,16 @@ module sub(input logic clk); real r; endmodule +module seq_event(input logic clk); + bit a, b, c; + sequence sq; + @(posedge clk) a ##1 b ##1 c; + endsequence + initial begin + @sq; + end +endmodule + package p; logic pkgvar; endpackage diff --git a/test_regress/t/t_dpi_decl.cpp b/test_regress/t/t_dpi_decl.cpp new file mode 100644 index 000000000..5cf783a39 --- /dev/null +++ b/test_regress/t/t_dpi_decl.cpp @@ -0,0 +1,22 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of either the GNU Lesser General Public License Version 3 +// or the Perl Artistic License Version 2.0. +// SPDX-FileCopyrightText: 2026 Antmicro +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +// +//************************************************************************* +#include "Vt_dpi_decl__Dpi.h" + +char* func(const char* arg) { + static char str[] = "abc"; + return str; +} +// some functions (e.g. getenv() from glibc) may have additional specifier, lack of it in the +// declaration will cause build errors +char* func_with_specifier(const char* arg) throw() { + static char str[] = "efd"; + return str; +} diff --git a/test_regress/t/t_dpi_decl.py b/test_regress/t/t_dpi_decl.py new file mode 100755 index 000000000..50cb5e61f --- /dev/null +++ b/test_regress/t/t_dpi_decl.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') +test.pli_filename = "t/t_dpi_decl.cpp" + +test.compile(v_flags2=[test.pli_filename]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_dpi_decl.v b/test_regress/t/t_dpi_decl.v new file mode 100644 index 000000000..1f5ae21c7 --- /dev/null +++ b/test_regress/t/t_dpi_decl.v @@ -0,0 +1,20 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Antmicro +// SPDX-License-Identifier: CC0-1.0 + +`define stop $stop +`define checks(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); + +module t; + import "DPI-C" function string func(input string arg) /*verilator dpi_c_decl "char* func(const char*)"*/; + import "DPI-C" function string func_with_specifier(input string arg) /*verilator dpi_c_decl "char* func_with_specifier(const char*) throw()"*/; + + initial begin + `checks(func("arg"), "abc"); + `checks(func_with_specifier("arg"), "efd"); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_dpi_decl_bad.out b/test_regress/t/t_dpi_decl_bad.out new file mode 100644 index 000000000..9a6dc9fde --- /dev/null +++ b/test_regress/t/t_dpi_decl_bad.out @@ -0,0 +1,5 @@ +%Error-BADVLTPRAGMA: t/t_dpi_decl_bad.v:8:57: No function declaration provided in /*verilator dpi_c_decl*/ meta-comment + 8 | import "DPI-C" function string func(input string arg) /*verilator dpi_c_decl*/; + | ^~~~~~~~~~~~~~~~~~~~~~~~ + ... For error description see https://verilator.org/warn/BADVLTPRAGMA?v=latest +%Error: Exiting due to diff --git a/test_regress/t/t_dpi_decl_bad.py b/test_regress/t/t_dpi_decl_bad.py new file mode 100755 index 000000000..bdec1e4cd --- /dev/null +++ b/test_regress/t/t_dpi_decl_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_dpi_decl_bad.v b/test_regress/t/t_dpi_decl_bad.v new file mode 100644 index 000000000..1d6466065 --- /dev/null +++ b/test_regress/t/t_dpi_decl_bad.v @@ -0,0 +1,14 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Antmicro +// SPDX-License-Identifier: CC0-1.0 + +module t; + import "DPI-C" function string func(input string arg) /*verilator dpi_c_decl*/; + + initial begin + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_force_release.py b/test_regress/t/t_force_release.py index 31b1f0e53..fd98eab68 100755 --- a/test_regress/t/t_force_release.py +++ b/test_regress/t/t_force_release.py @@ -11,7 +11,7 @@ import vltest_bootstrap test.scenarios('simulator') -test.compile(verilator_flags2=["--binary"]) +test.compile(verilator_flags2=["--binary", "-Wno-IEEEMAYDEPRECATE"]) test.execute() diff --git a/test_regress/t/t_force_release.v b/test_regress/t/t_force_release.v index fe78ccfcd..34a3e60b7 100644 --- a/test_regress/t/t_force_release.v +++ b/test_regress/t/t_force_release.v @@ -5,7 +5,7 @@ // SPDX-License-Identifier: CC0-1.0 // verilog_format: off -`define stop // TODO +`define stop $stop `define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) // verilog_format: on @@ -14,11 +14,12 @@ module t; logic a, b, c, d; wire e; + and and1 (e, a, b, c); initial begin $monitor("%d d=%b,e=%b", $stime, d, e); - d = a & b & c; + assign d = a & b & c; a = 1; b = 0; c = 1; diff --git a/test_regress/t/t_property_delay_large.py b/test_regress/t/t_property_delay_large.py new file mode 100755 index 000000000..91edb539c --- /dev/null +++ b/test_regress/t/t_property_delay_large.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.sim_time = 2700 + +test.compile(timing_loop=True, + verilator_flags2=['--assert', '--timing', '--coverage-user', '--dumpi-graph', '6']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_property_delay_large.v b/test_regress/t/t_property_delay_large.v new file mode 100644 index 000000000..6e6752f42 --- /dev/null +++ b/test_regress/t/t_property_delay_large.v @@ -0,0 +1,73 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Antmicro +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv, expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +`define checkp(gotv,expv_s) do begin string gotv_s; gotv_s = $sformatf("%p", gotv); if ((gotv_s) != (expv_s)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv_s), (expv_s)); `stop; end end while(0); +// verilog_format: on + +module t ( + input clk +); + localparam int BIG_DELAY = 1024; + localparam int END_CYC = BIG_DELAY + 300; + + int cyc = 0; + int fixed_pass_q[$]; + int fixed_fail_q[$]; + int range_pass_q[$]; + int range_fail_q[$]; + + // Fixed delay: starts 1..12. Odd starts pass; even starts fail. + wire fixed_a = (cyc >= 1 && cyc <= 12); + wire fixed_b = (cyc == BIG_DELAY + 1) || (cyc == BIG_DELAY + 3) + || (cyc == BIG_DELAY + 5) || (cyc == BIG_DELAY + 7) + || (cyc == BIG_DELAY + 9) || (cyc == BIG_DELAY + 11); + + // Range delay: single passing starts match 10 cycles later. Two blocks never + // match and expire 300 cycles after each start. + wire range_pass_a = (cyc == 10) || (cyc == 40) || (cyc == 70) || (cyc == 100) + || (cyc == 130) || (cyc == 600) || (cyc == 640) + || (cyc == 680) || (cyc == 720); + wire range_fail_a = (cyc >= 250 && cyc <= 257) || (cyc >= 820 && cyc <= 827); + wire range_a = range_pass_a || range_fail_a; + wire range_b = (cyc == 20) || (cyc == 50) || (cyc == 80) || (cyc == 110) + || (cyc == 140) || (cyc == 610) || (cyc == 650) || (cyc == 690) + || (cyc == 730); + + // Questa action blocks observe the cycle after the sampled property cycle. + cover property (@(posedge clk) fixed_a ##1024 fixed_b) fixed_pass_q.push_back($sampled(cyc) + 1); + + assert property (@(posedge clk) fixed_a |-> ##1024 fixed_b) + else fixed_fail_q.push_back($sampled(cyc) + 1); + + cover property (@(posedge clk) range_a ##[5:300] range_b) range_pass_q.push_back($sampled(cyc) + 1); + + assert property (@(posedge clk) range_a |-> ##[5:300] range_b) + else range_fail_q.push_back($sampled(cyc) + 1); + + assert property (@(posedge clk) + 1'b0 |-> (fixed_a throughout (range_a throughout (range_b ##40 fixed_b)))); + + assert property (@(posedge clk) 1'b0 |-> ##[5:300] (range_b ##1 fixed_b)); + + assert property (@(posedge clk) 1'b0 |-> (fixed_a throughout (range_a ##[5:300] range_b))); + + always_ff @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == END_CYC) begin + `checkp(fixed_pass_q, "'{'h402, 'h404, 'h406, 'h408, 'h40a, 'h40c}"); + `checkp(fixed_fail_q, "'{'h403, 'h405, 'h407, 'h409, 'h40b, 'h40d}"); + `checkp(range_pass_q, "'{'h15, 'h33, 'h51, 'h6f, 'h8d, 'h263, 'h28b, 'h2b3, 'h2db}"); + `checkp(range_fail_q, + "'{'h227, 'h228, 'h229, 'h22a, 'h22b, 'h22c, 'h22d, 'h22e, 'h461, 'h462, 'h463, 'h464, 'h465, 'h466, 'h467, 'h468}"); + + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_property_sexpr_multi.py b/test_regress/t/t_property_sexpr_multi.py index 5de1b48bd..7f63922fc 100755 --- a/test_regress/t/t_property_sexpr_multi.py +++ b/test_regress/t/t_property_sexpr_multi.py @@ -11,7 +11,7 @@ import vltest_bootstrap test.scenarios('simulator') -test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing', '--dumpi-V3AssertProp 6']) +test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing']) test.execute() diff --git a/test_regress/t/t_property_sexpr_range_delay.v b/test_regress/t/t_property_sexpr_range_delay.v index cd3037d03..929568fda 100644 --- a/test_regress/t/t_property_sexpr_range_delay.v +++ b/test_regress/t/t_property_sexpr_range_delay.v @@ -13,6 +13,8 @@ module t ( input clk ); + parameter P = 1; + integer cyc = 0; reg [63:0] crc = '0; reg [63:0] sum = '0; @@ -64,6 +66,11 @@ module t ( assert property (@(posedge clk) disable iff (cyc < 2) a |-> ##[1:3] (a | b | c | d | e)); + // Parameterized range bound + assert property (@(posedge clk) disable iff (cyc < 2) + a |-> ##[P:P+3] (a | b | c | d | e)); + assert property (@(posedge clk) ##[P:P+3] 1); + // ##[2:4] range delay assert property (@(posedge clk) disable iff (cyc < 2) b |-> ##[2:4] (a | b | c | d | e)); @@ -80,6 +87,9 @@ module t ( assert property (@(posedge clk) disable iff (cyc < 2) a |-> ##[1:10000] (a | b | c | d | e)); + cover property (@(posedge clk) disable iff (cyc < 2) + ##[0:10000] 1'b1); + // Range with binary SExpr: nextStep has delay > 0 after range match assert property (@(posedge clk) disable iff (cyc < 2) a |-> b ##[1:2] (a | b | c | d | e) ##3 (a | b | c | d | e)) diff --git a/test_regress/t/t_sequence_ref_bad.out b/test_regress/t/t_sequence_ref_bad.out new file mode 100644 index 000000000..afe2fe2f7 --- /dev/null +++ b/test_regress/t/t_sequence_ref_bad.out @@ -0,0 +1,7 @@ +%Error: t/t_sequence_ref_bad.v:14:6: Concurrent assertion has no clock (IEEE 1800-2023 16.16) + : ... note: In instance 't' + : ... Suggest provide a clocking event, a default clocking, or a clocked procedural context + 14 | @s_one; + | ^~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: Exiting due to diff --git a/test_regress/t/t_sequence_ref_unsup.py b/test_regress/t/t_sequence_ref_bad.py similarity index 100% rename from test_regress/t/t_sequence_ref_unsup.py rename to test_regress/t/t_sequence_ref_bad.py diff --git a/test_regress/t/t_sequence_ref_unsup.v b/test_regress/t/t_sequence_ref_bad.v similarity index 100% rename from test_regress/t/t_sequence_ref_unsup.v rename to test_regress/t/t_sequence_ref_bad.v diff --git a/test_regress/t/t_sequence_ref_unsup.out b/test_regress/t/t_sequence_ref_unsup.out deleted file mode 100644 index 41fb50aad..000000000 --- a/test_regress/t/t_sequence_ref_unsup.out +++ /dev/null @@ -1,6 +0,0 @@ -%Error-UNSUPPORTED: t/t_sequence_ref_unsup.v:9:12: Unsupported: sequence referenced outside assertion property - : ... note: In instance 't' - 9 | sequence s_one; - | ^~~~~ - ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error: Exiting due to