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/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 06b668c2d..884d1a1f9 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -71,10 +71,12 @@ jobs: # behind 'verilator --version' fetch-depth: ${{ (inputs.install || inputs.dev-gcov) && '0' || '1' }} - # ccache is unreliable on the macOS runners; disable it there - - name: Disable ccache on macOS - if: ${{ startsWith(inputs.runs-on, 'macos') }} - run: echo "CCACHE_DISABLE=1" >> "$GITHUB_ENV" + - 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' }} diff --git a/.github/workflows/reusable-lint-py.yml b/.github/workflows/reusable-lint-py.yml index be0e60cd3..e3bea6fe7 100644 --- a/.github/workflows/reusable-lint-py.yml +++ b/.github/workflows/reusable-lint-py.yml @@ -23,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-test.yml b/.github/workflows/reusable-test.yml index 8964cabd2..bd5a76c7c 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -42,7 +42,6 @@ env: CCACHE_COMPILERCHECK: content CCACHE_DIR: ${{ github.workspace }}/.ccache CXX: ${{ inputs.cc == 'clang' && 'clang++' || 'g++' }} - GH_TOKEN: ${{ github.token }} defaults: run: @@ -68,78 +67,31 @@ jobs: tar --zstd -x -f ${{ inputs.archive }} ls -lsha - # Test-job ccache is stored as an artifact, not actions/cache - # - pull_request: restore this PR's own previous run; if none, fall back to the PR's target branch. - # - branch push: restore that branch's own cache - # Artifact names are a global, unauthenticated namespace, so a PR could - # upload a "-branch-" artifact impersonating a branch. Branch-scoped - # restores are therefore provenance-checked below (must come from a same-repo - # push to that branch); the pr- scope is not (a PR can only affect its own - # unmerged cache). - - name: Compute ccache artifact name - # Pass github refs via env, not ${{ }} interpolation, so a branch name - # containing shell metacharacters can't break or inject into the script. - env: - BASE_REF: ${{ github.event.pull_request.base.ref }} - REF_NAME: ${{ github.ref_name }} + - name: Configure ccache run: | - base="ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.suite }}" - sanitize() { printf '%s' "$1" | tr '/' '_'; } # artifact names cannot contain '/' - if [ "${{ github.event_name }}" = pull_request ]; then - echo "CCACHE_ARTIFACT=${base}-pr-${{ github.event.pull_request.number }}" >> "$GITHUB_ENV" - echo "CCACHE_ARTIFACT_BRANCH=" >> "$GITHUB_ENV" - echo "CCACHE_FALLBACK=${base}-branch-$(sanitize "$BASE_REF")" >> "$GITHUB_ENV" - echo "CCACHE_FALLBACK_BRANCH=${BASE_REF}" >> "$GITHUB_ENV" - else - echo "CCACHE_ARTIFACT=${base}-branch-$(sanitize "$REF_NAME")" >> "$GITHUB_ENV" - echo "CCACHE_ARTIFACT_BRANCH=${REF_NAME}" >> "$GITHUB_ENV" + # 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: Restore ccache from artifact - continue-on-error: true # a cold start is fine; never fail the job here - run: | - set -euo pipefail - mkdir -p "$CCACHE_DIR" - # Return newest non-expired artifact named $1 (empty if none). When - # $2 (branch) is non-empty, require provenance: the artifact must come from - # a same-repo run (head_repository_id == repository_id) whose head branch - # is $2, so a fork PR cannot forge a branch-scoped artifact. - 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 ccache from '$name' (run $rid)" - gh run download "$rid" --name "$name" --dir "$RUNNER_TEMP/ccache-dl" || return 1 - tarball="$(find "$RUNNER_TEMP/ccache-dl" -name ccache.tar.zst -print -quit)" - [ -n "$tarball" ] || return 1 - tar -I zstd -x -f "$tarball" -C "$CCACHE_DIR" - } - if restore "${CCACHE_ARTIFACT:-}" "${CCACHE_ARTIFACT_BRANCH:-}"; then - echo "Restored primary ccache" - exit 0 - fi - if restore "${CCACHE_FALLBACK:-}" "${CCACHE_FALLBACK_BRANCH:-}"; then - echo "Restored fallback ccache" - exit 0 - fi - echo "No ccache artifact found; starting cold" + # 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: 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 @@ -164,19 +116,15 @@ jobs: path: ${{ github.workspace }}/repo/obj_coverage/verilator-${{ inputs.suite }}.info name: code-coverage-${{ inputs.suite }} - - name: Pack ccache - if: ${{ !cancelled() && env.CCACHE_ARTIFACT != '' }} - run: tar -I 'zstd -T0' -cf "$GITHUB_WORKSPACE/ccache.tar.zst" -C "$CCACHE_DIR" . - - - name: Save ccache artifact - if: ${{ !cancelled() && env.CCACHE_ARTIFACT != '' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + - name: Save ccache + if: ${{ env.CCACHE_DISABLE != '1' && !cancelled() }} + uses: ./repo/.github/actions/artifact-cache with: - name: ${{ env.CCACHE_ARTIFACT }} - path: ${{ github.workspace }}/ccache.tar.zst - retention-days: 3 - overwrite: true - compression-level: 0 + 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() }} diff --git a/ci/ci-build.bash b/ci/ci-build.bash index e9c2b2a20..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 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 52183f7bf..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,8 +66,8 @@ 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 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/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..b96696cff 100644 --- a/src/V3AssertNfa.cpp +++ b/src/V3AssertNfa.cpp @@ -287,6 +287,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 +296,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 +472,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))}; @@ -574,8 +585,7 @@ 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; } @@ -881,8 +891,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 +955,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 +1450,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() { @@ -2876,11 +2885,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 +2907,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 +2951,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; 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/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/V3Width.cpp b/src/V3Width.cpp index 3c2db0757..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); 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/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_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_property_sexpr_range_delay.v b/test_regress/t/t_property_sexpr_range_delay.v index cd3037d03..85ad2dcc8 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)); 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